AIsaAIsaStudio English 打开 AIsa Studio

Microsoft Research 发布 PDD Kit,形式化验证智能体编写的全栈 Web 应用

原标题:Formally verifying agent-written full-stack web apps with PDD Kit

Microsoft Research 发布 PDD Kit(Proof-Driven Development 工具包),将意图转化为 Verus 形式化规范,对智能体编写的代码逐输入证明其满足规范,证明无法覆盖处回退到测试。团队在 GitHub Next 的 Chopin 中对约 5,000 行 Rust 全栈应用 Pet Store 的登录逻辑完成形式化验证,开发者无需逐行审读生成代码即可合并;pdd-ship 工作流将 Verus 检查加入 CI,后续改动须持续通过同一规范证明。PDD Kit 目前为研究原型,仅支持验证 Rust。

为什么值得看

微软研究团队展示了用 PDD Kit 对智能体编写的代码做形式化验证的完整流程,并公开了验证边界与信任假设的划分方式。

阅读原文 ↗11月6日 AI 日报

本页的摘要、推荐理由与解读出自 AIsa Studio 的 AI 风向智能体,原文版权归原作者所有。

深度解读

发生了什么

Microsoft Research 发布了 PDD Kit,一套用于「证明驱动开发」(Proof-Driven Development, PDD)的工具包,目标是对编码智能体编写的代码进行形式化验证,并将其应用到日常软件而非小众专业领域。团队在一个完整的全栈 Web 应用上做了演示:在 GitHub Next 的 Chopin 环境中,对完全用 Rust 编写的 Pet Store 店面应用的登录逻辑进行了形式化验证。

关键细节

  • PDD Kit 运行在 Chopin(GitHub Next 的实时多人规划环境)中,工作流分两半:团队成员用自然语言确认意图,PDD Kit 提出细心的审查者会问的问题,把可形式化的意图转成 Verus 形式规范,其余转为测试用的非正式规范。
  • 验证分两类并明确标注:形式验证是数学证明,对每个可能输入成立,由 Verus(微软研究院与卡内基梅隆大学等多家机构共同开发的 Rust 证明语言)检查;经验验证靠测试、终端自动化、浏览器和计算机使用智能体,PDD Kit 将其标注为「证据」而非证明。
  • 演示中,开发者在 Chopin 里的全部请求只有两句话:验证 auth.rs 的登录逻辑,且「只有密码正确才拿到令牌」「只有管理员账户拿到管理员令牌」。PDD Kit 的规划流程在写任何代码前先提出决策问题——例如「只有密码正确才发令牌」这句话同样会被一个拒绝所有登录的实现在字面上满足,需要开发者选择「双向」约束,才能构成当且仅当。
  • 证明覆盖不到的部分被明确划入可信计算基础(TCB):bcrypt 哈希、sqlx 查询、jsonwebtoken 签名等外部 crate、数据库与时钟等外部世界事实、是否完成与耗时、需要人判断的体验问题、以及规范之外未写下的需求。开发者逐项签署确认了六个可信组件,并为每个假设条款在计划中写入测试。
  • 两句需求最终变成 login_flow 上的八个 ensures 子句,使用未解释函数(如 customer_on_file、bcrypt_verdict)和从不运行、会被编译器擦除的 ghost code 来建模外部世界。规范能捕捉「密码针对一个账户校验、令牌却签给另一个账户」这类测试套件很少想到的接线错误。
  • 签署规范后,实现和证明全部由机器检查,开发者无需逐行阅读生成的代码或证明。pdd-prove 流程中还专门检查「氛围验证」(vibe verification)——模型自己写规范时倾向于产生验证通过但几乎不排除任何情况的弱不变式。
  • Pet Store 约 5,000 行 Rust,后端用 Axum 和 SQLite,店面为编译到 WebAssembly 的 Leptos 应用。验证后 Verus 强化了代码并「发现了现有的 bug」,证明了相对于规范的登录 bug 缺失性。

为什么重要

原文指出,编码智能体现在「常常一口气交付跨越数万乃至数十万行代码的完整功能」,逐行阅读 PR 的审查方式不再可扩展。团队转而投入计划和测试,但「计划本应是你与编码智能体之间的契约,却没有任何东西强制智能体遵守它」,智能体可能偏离计划并为自己的实现而非你想要的代码编写测试,CI 覆盖率看起来健康,差距却一直隐藏到 bug 报告出现。

测试只触及程序能做之事的极小切片,而证明回答的是「这是否正确」而非「这看起来是否正确」,能为指定属性提供比测试更强的保证。过去证明对日常软件过于昂贵——需要专家、陌生的语言、比代码大数倍的证明和数年开发;如今编码智能体的进步让从规范生成证明、以及帮人写规范都变得可行。PDD Kit 的意义在于把这个验证边界(证明什么、信任什么、无人承诺什么)显式化并交由人逐项确认,同时不要求使用者具备形式化方法背景。

对谁有影响

对依赖编码智能体生成大量代码、需要规模化审查的开发团队最为直接;对维护含登录、权限等安全敏感逻辑的应用开发者,PDD Kit 建议从最敏感部分入手增量验证。使用 Rust 技术栈(Axum、SQLite、Leptos 等)的团队能直接受益于 Verus 生态,而关注 AI 时代软件质量保证方法论的人也能从中看到形式化方法走向日常工程的一条路径。

全文翻译

以下翻译只覆盖原文的前一部分,其余内容请看下方原文。

PDDKit 决策流程图,背景为绿色渐变

概览

  • 面向日常软件的形式化方法。 PDD Kit 可帮助您将意图转化为形式化规范,然后对代理(agent)为每个输入编写的代码进行证明,并在证明无法覆盖的地方使用测试。它将形式化方法从小众的专业工作扩展到没有形式化方法背景的工程师以及日常应用程序中,例如此处展示的全栈 Web 应用。
  • 随着 AI 代理生成越来越多的代码,逐行的人类审查变得难以扩展。 计划本应是您与编码代理之间的契约,但没有任何东西真正迫使代理遵循该计划。它编写的测试通常检查它构建的内容,而不是您要求的内容。
  • PDD Kit 确保代理遵守计划。 它与协作将您的意图转化为 Verus (在新标签页中打开) 中的形式化规范,证明代理的代码针对每个输入都符合该规范,并在证明无法覆盖的地方回退到测试。
  • 我们在一个全栈 Web 应用中尝试了它。 在 GitHub Next 的 Chopin (在新标签页中打开) 内部,我们使用 PDD Kit 对 Pet Store(一个用 Rust (在新标签页中打开) 编写的商店)的登录逻辑进行了形式化验证。Verus 加固了代码并发现了现有漏洞,证明了相对于规范不存在登录漏洞。开发人员仅审查了意图,并未逐行阅读生成的代码。

计划是代理应遵循的契约,但没有任何约束强制代理遵循它。

编码代理现在经常一次性交付整个功能,往往涉及数万甚至数十万行代码。因此,阅读拉取请求中的每一行已无法适应这种新的工作方式。目前,大多数团队投资于计划和相关的测试:向代理描述他们想要的内容,反复沟通直到计划和拟议的测试看起来正确,然后将它们交给代理进行实现。

这个计划或自然语言规范旨在成为代理持有的契约。然而,在实践中,代理可能会偏离它。它实现了略有不同的内容,然后编写通过其实现的测试,而不是您打算让它编写的代码。由于大量低质量和高数量的测试,持续集成(CI)规模扩大,覆盖率看起来很健康,而您要求的与最终交付之间的差距一直隐藏,直到出现错误报告或棘手的边缘情况才暴露出来。

那么,当代理生成越来越多的代码时,如何可靠地扩展审查?代理代码以两种谨慎的审查者通常会发现的方式失败:遗漏了明显必要的部分,或者做了明显错误的事情。仅靠测试无法弥补这一差距,因为它们只涵盖了程序可能执行的一小部分。弥补这一差距需要不同类型的机制,这就是证明发挥作用的地方。

播客系列

Doug Burger 的插图

未来的形态

加入微软的 Doug Burger 和嘉宾,深入探讨关于 AI 的基本真理及其将如何重塑未来。

审查问的是“这看起来对吗?”证明回答的是“这确实对吗?”数学证明显示代码针对 每一个 输入都符合其规范,包括您的测试套件可能遗漏的输入。这比单独使用测试为指定属性提供了更强的保证,尤其是在逐行审查不切实际的情况下。

过去,证明对于日常软件来说成本太高。您需要专家、陌生的语言、比代码大得多的证明,并且证明的开发需要数年,但自那以后有两件事发生了变化。首先,编码代理的最新进展使得它们越来越实用地帮助从定义良好的规范中生成形式化证明。其次,代理现在可以更好地帮助您编写形式化规范。我们将这种新的软件开发范式称为 驱动式开发(Proof-Driven Development, PDD)。PDD Kit 是我们的工具包。

PDD Kit 的工作原理

PDD Kit 之于形式化方法,大致相当于 Spec Kit (在新标签页中打开) 之于规范驱动开发(Spec-Driven Development)。它在 Chopin (在新标签页中打开) 内作为合作伙伴运行,这是 GitHub Next 的实时多人规划环境,团队和代理可以在一个共享文档中进行规划。

工作流程分为两半(图 1)。首先,团队成员以纯语言审查意图。PDD Kit 提出谨慎审查者会提出的问题,在过程中解释相关代码,并将答案记录在 意图工件(intent artifact) 中,这是一个连接用户意图与形式化规范或测试的文档。可以形式化的用户意图部分被转换为形式化规范,其余必须信任的部分被转换为用于测试的非正式规范。PDD Kit 自动生成证明代码满足派生形式化规范的证明,并为非正式规范生成高质量测试。您不需要任何形式化方法背景即可完成所有这些操作。

显示 PDD Kit 工作流程的流程图,用于形式化验证代理编写的代码。团队成员通过以纯语言表达需求来审查意图,这些需求被形式化为包含决策卡、意图工件、可信边界和审查员批准的 pdd-plan。该计划生成带有 Verus ensures 子句的形式化规范。机器随后通过 pdd-prove 检查代码,其中包括 Verus 证明、对可信组件的测试以及 LLM 间隙审查。经过验证的代码和收据作为拉取请求提交。虚线反馈循环将规范矛盾和验证间隙返回到早期的规划和编码阶段。

图 1. 使用 PDD Kit 进行驱动式开发。团队成员确定意图。Verus 和测试套件使代码符合该规范。

因此,PDD Kit 中的每个结果都说明其基于哪种类型的验证:

  • 形式化验证 是一种数学证明,表明代码针对每个可能的输入都符合其规范,由称为验证器的工具进行检查。对于今天的 PDD Kit,该验证器是 Verus (在新标签页中打开),这是一种由微软研究院、卡内基梅隆大学、VMware Research、苏黎世联邦理工学院、MPI-SWS、新南威尔士大学悉尼分校、德克萨斯大学奥斯汀分校、密歇根大学、不列颠哥伦比亚大学、威斯康星大学麦迪逊分校和魏茨曼科学研究所共同开发的 Rust 证明语言。
  • 经验验证 通过运行代码并观察其行为来检查代码。它覆盖了证明无法触及的地方,例如第三方加密库或用户界面。它使用单元测试和端到端测试、终端自动化、浏览器代理以及浏览器无法驱动的任何内容的计算机使用代理。通过的测试仅表明代码在其尝试的输入上有效,因此 PDD Kit 将其标记为证据而非证明。

两者之间的界限是 可信计算基(Trusted Computing Base, TCB):证明假设按描述行为的代码。PDD Kit 使这条线明确化,并要求您对每一部分签字确认。

压轴之作:一个全栈 Web 应用

为了展示 PDD Kit 如何与现有代码库集成,我们构建了 Pet Store,这是一个完全用 Rust (在新标签页中打开) 编写的全栈 Web 应用。后端使用 Axum (在新标签页中打开) 和 SQLite (在新标签页中打开),处理库存、促销、订单和结账。商店前端是一个编译为 WebAssembly (在新标签页中打开) 的 Leptos (在新标签页中打开) 应用。它们总共约有 5,000 行 Rust 代码。

为了在本博客中保持简洁,我们专注于形式化验证应用的登录逻辑,但相同的过程可用于验证整个应用的功能正确性。PDD Kit 建议从应用程序中最敏感的部分开始,并逐步验证您的应用程序。登录漏洞可能会让攻击者获得客户的账户或管理员的控制权,因此证明努力在此处最先见效。开发人员在 Chopin 中的整个请求只有两句话:

Verify the login logic in backend/src/handlers/auth.rs. Here are my requirements:

  1. You only get a token with the right password.
  2. Only admin accounts get admin tokens.

我们故意保持请求的大致性。捕获用户意图并将用户请求转化为证明可以检查的内容是 PDD Kit 的工作。

第 1 步:代码之前的问题

PDD Kit 的规划工作流 pdd-plan 在阅读代码后,在编写任何内容之前,会以 Decisions 选项卡的形式返回问题。当问题取决于代码的工作方式时,卡片会先解释那部分:目标、各部分如何配合以及涉及的确切函数,以便您可以在不打开源代码的情况下回答问题。

显示验证工具评估登录认证要求的屏幕截图。提示询问:“正确的密码是否总是获得令牌?”流程图显示登录请求导致密码检查,匹配的密码接收令牌,不匹配的密码被拒绝。选项 A 被高亮显示为推荐,证明两种结果:正确的密码接收令牌,错误的密码被拒绝。选项 B 仅证明错误的密码被拒绝,留下正确密码的结果未指定。

图 2. 第一个决策卡。“仅使用正确的密码”本身也适用于拒绝所有人的登录。证明两种方式排除了这种情况。

这个问题看似迂腐,但很重要。“你只有在正确的密码下才能获得令牌”排除了坏的情况,即错误的密码获得令牌,而拒绝所有人的登录完美地保持了这一承诺。“正确的密码总是有效”是排除它的另一半。形式上,前者是蕴含(token ⇒ right password),后者是其逆命题。结合起来构成“当且仅当”。如果放任不管,代理会选择其中一种而不告诉你,但在这里,开发人员选择了两种方式。

第 2 步:人类可读的意图地图

随着对话的进行,PDD Kit 在计划顶部保持 rough work 部分最新,以便用户可以跟踪重要概念,而无需滚动长聊天历史查找已达成共识的内容。它包括正在指定的登录的流程图、将被证明和被信任的规范景观,以及在图片有帮助时的心理模型。对于登录,PDD Kit 绘制了一个售票窗口(图 3)。

三面板插图比较登录结果。在第一面板中,客户使用正确的电子邮件和密码登录并获得“customer”票。在第二面板中,相同的电子邮件与错误的密码配对,访问被拒绝。在第三面板中,管理员使用正确的用户名和密码登录并获得“admin”票。该图表使用售票柜台类比来解释身份验证和基于角色的访问。

图 3. PDD Kit 的登录心理模型。您发送的字段选择窗口,窗口选择桌子及其角色,直到密码匹配才会打印任何东西。

在其下方,_intent m

查看原文

PDDKit decision flowchart on green gradient background

At a glance

  • Formal methods for everyday software. PDD Kit helps you turn your intent into a formal spec, then proves the code your agent writes for every input and uses tests where a proof can’t reach. It takes formal methods beyond niche, specialist work to engineers without a formal methods background and to everyday applications, like the full-stack web app shown here.
  • Exhaustive line-by-line human review can become difficult to scale as AI agents generate increasingly large amounts of code. The plan is supposed to be the contract between you and a coding agent, but nothing actually forces the agent to follow it. The tests it writes usually check what it built, rather than what you asked for.
  • PDD Kit makes the agent keep the plan. It works with you to turn your intent into a formal specification in Verus (opens in new tab), proves the agent’s code meets that spec for every input, and falls back to tests where a proof can’t reach.
  • We tried it on a full-stack web app. Inside GitHub Next’s Chopin (opens in new tab), we used PDD Kit to formally verify the login logic of Pet Store, a Rust (opens in new tab) storefront. Verus hardened the code and uncovered existing bugs, proving the absence of login bugs with respect to the specification. The developer reviewed the intent and never read the generated code line-by-line.

The plan is the contract the agent should follow, but nothing constrains the agent to follow it.

Coding agents now ship whole features in one sitting often spanning tens of thousands or hundreds of thousands of lines of code. As a result, reading every line of the pull request doesn’t scale to this new way of work. Now, most teams invest in the plan and relevant testing instead: describe to an agent what they want, go back and forth until the plan and proposed tests look right, and hand them off to the agent for implementation.

This plan or natural language spec is meant to be the contract that an agent holds. However, in practice, the agent can drift from it. It implements something slightly different, then writes tests that pass for its implementation rather than the code you intended it to write. Continuous integration (CI) grows due to a profusion of low quality and high quantity of tests, coverage looks healthy, and the gap between what you asked for and what shipped stays hidden until a bug report or a nasty edge case shows up.

So, how do you reliably scale review as agents generate increasingly large amounts of code? Agent code fails in two ways a careful reviewer would usually catch: it leaves out something obviously necessary, or it does something obviously wrong. Tests alone don’t close that gap, because they only exercise a tiny slice of what a program can do. Closing it needs a different kind of machinery, which is where proofs come in.

PODCAST SERIES

Illustration of Doug Burger

The Shape of Things to Come

Join Microsoft’s Doug Burger and guests as they dig into the fundamental truths about AI and how it will reshape the future.

Review asks “does this look right?” A proof answers “is this right?” A mathematical proof shows that code meets its specification for every input, including the ones your test suite can miss. That provides stronger assurance for specified properties than testing alone, especially when line-by-line review isn’t practical.

Proofs used to be too expensive for everyday software. You needed specialists, unfamiliar languages, proofs many times larger than the code, and proofs took years to develop, but two things have changed since. First, recent advances in coding agents have made it increasingly practical for them to help generate formal proofs from well-defined specs. Secondly, agents can now better help you write formal specs. We call this new software development paradigm Proof-Driven Development (PDD). PDD Kit is our toolkit for it.

How PDD Kit works

PDD Kit is to formal methods roughly what Spec Kit (opens in new tab) is to Spec-Driven Development. It runs as a partner inside Chopin (opens in new tab), GitHub Next’s real-time, multiplayer planning environment, where a team and agents can plan in one shared document.

The workflow has two halves (Figure 1). First, team members review intent in plain language. PDD Kit asks the questions a careful reviewer would ask, explains the relevant code as it goes, and records the answers in an intent artifact, a document that bridges the user’s intent with the formal specification or tests. The parts of the user’s intent that can be formalized are converted into formal specifications and the rest, which must be taken on trust, are converted to informal specifications for tests. PDD Kit automatically generates proofs that the code satisfies the derived formal specs and high-quality tests for the informal specs. You don’t need a formal methods background for any of it.

Flow diagram showing the PDD Kit workflow for formally verifying agent-written code. Team members review intent by expressing requirements in plain language, which are formalized in a pdd-plan containing decision cards, intent artifacts, trusted boundaries, and reviewer approvals. The plan generates a formal specification with Verus ensures clauses. Machines then check the code through pdd-prove, which includes Verus proofs, tests for trusted components, and LLM gap review. Verified code and receipts are submitted as a pull request. Dashed feedback loops return specification contradictions and verification gaps to earlier planning and coding stages.

Figure 1. Proof-Driven Development with PDD Kit. Team members settle intent. Verus and the test suite hold the code to it.

Consequently, every result in PDD Kit says which kind of verification it rests on:

  • Formal verification is a mathematical proof that code satisfies its specification for every possible input, checked by a tool called a verifier. For PDD Kit today, that verifier is Verus (opens in new tab), a proof language for Rust co-developed by Microsoft Research, Carnegie Mellon University, VMware Research, ETH Zurich, MPI-SWS, UNSW Sydney, the University of Texas at Austin, the University of Michigan, the University of British Columbia, the University of Wisconsin–Madison and the Weizmann Institute of Science.
  • Empirical verification checks code by running it and observing what it does. It covers what a proof can’t reach, such as a third-party cryptography library or a user interface. It uses unit and end-to-end tests, terminal automation, browser agents, and computer-use agents for anything a browser can’t drive. A passing test only shows the code worked on the inputs it tried, so PDD Kit labels it as evidence rather than proof.

The line between the two is the Trusted Computing Base (TCB): the code that the proof assumes behaves as described. PDD Kit makes that line explicit and asks you to sign off on each piece.

The capstone: a full-stack web app

To show PDD Kit integrating with existing codebases, we built Pet Store, a full-stack web app written entirely in Rust (opens in new tab). The backend uses Axum (opens in new tab) and SQLite (opens in new tab) and handles inventory, promotions, orders and checkout. The storefront is a Leptos (opens in new tab) app compiled to WebAssembly (opens in new tab). Together they come to about 5,000 lines of Rust.

For brevity in this blog, we focus on formally verifying the login logic of the app, but the same process can be used to verify functional correctness of the entire app. PDD Kit recommends starting with the most sensitive parts of your application and incrementally verifying your application. A login bug can hand an attacker a customer’s account or an admin’s controls, so that’s where proof effort pays off first. The developer’s whole request in Chopin was two sentences:

Verify the login logic in backend/src/handlers/auth.rs. Here are my requirements:

  1. You only get a token with the right password.
  2. Only admin accounts get admin tokens.

We kept the request rough on purpose. Capturing the intent of the user and turning the user request into something a proof can check is PDD Kit’s job.

Step 1: Questions before code

PDD Kit’s planning workflow, pdd-plan, reads the code and comes back, before writing anything, with questions in a Decisions tab. When a question depends on how the code works, the card explains that part first: the goal, how the pieces fit, and the exact function involved, so you can answer without opening the source.

Screenshot of a verification tool evaluating a login authentication requirement. The prompt asks, “Must the right password always get a token?” A flow diagram shows a login request leading to a password check, where matching passwords receive a token and non-matching passwords are refused. Option A, highlighted as recommended, proves both outcomes: the correct password receives a token and an incorrect password is refused. Option B proves only that incorrect passwords are refused, leaving the result for the correct password unspecified.

Figure 2. The first decision card. “Only with the right password” on its own is also met by a login that refuses everyone. Proving both ways rules that out.

The question looks pedantic, but it matters. “You only get a token with the right password” rules out the bad case, a wrong password getting a token, and a login that refuses everyone keeps that promise perfectly. “The right password always works” is the half that rules it out. Formally, the first is an implication (token ⇒ right password) and the second is its converse. Together they make an “if and only if.” An agent left alone would pick one without telling you, but here the developer picked both ways.

Step 2: A human readable intent map

As the conversation goes on, PDD Kit keeps a rough work section at the top of the plan current, so a user can keep track of important concepts without having to scroll back through a long chat to find what was agreed. It includes a flowchart of the login being specified, a spec landscape of what will be proved and what will be trusted, and, when a picture helps, a mental model. For login, PDD Kit drew a ticket desk (Figure 3).

Three-panel illustration comparing login outcomes. In the first panel, a customer logs in with the correct email and password and receives a “customer” ticket. In the second panel, the same email is paired with an incorrect password and access is refused. In the third panel, an administrator logs in with the correct username and password and receives an “admin” ticket. The diagram uses a ticket-counter analogy to explain authentication and role-based access.

Figure 3. PDD Kit’s mental model for login. The field you send picks the window, the window picks the table and with it the role, and nothing prints until the password matches.

Below it, the intent map ties each requirement back to the developer’s own words and records how it was settled (Figure 4). Each entry links to the section of the formal spec that models it and to the tests that cover it, and the spec section shows snippets of the Verus contract, annotated so the developer can see how the formal spec models their intent.

Intent map showing the login authentication flow for issuing a token. If an account exists and bcrypt verifies the correct password, the user receives claims for that account. If the account does not exist or the password is incorrect, the result is “401 Unauthorized.” If the password hash cannot be read, the result is “500 unreadable hash.”

Figure 4. An intent map entry after the decision: a login gets a token only with the right password for that account, and always with it. The flowchart shows every way the password check can end.

The decision card in Figure 2 is the kind of ambiguity an agent would otherwise settle silently. Figure 5 shows how small the difference looks on paper: the two readings disagree in a single cell, a right password that gets no token.

Side-by-side comparison of two authentication policies. The left panel, “Only when,” shows that a token implies the correct password but does not guarantee a token for a correct password. The right panel, “Exactly when,” shows a two-way relationship in which a token is issued if and only if the correct password is provided. The highlighted difference is that denying a token to a user with the correct password is allowed under the first policy but forbidden under the second.

Figure 5. Two readings of the same sentence. They differ in one cell, and that cell changes what the proof has to establish.

Step 3: Proving the login function

Before anything gets built, PDD Kit asks the team to sign off on the plan as a whole. Its first draft proved the decision at the center of login, picking the table and gating on the password, and kept the handler around it trusted and tested. That’s a sensible default, and it’s what PDD Kit recommended, but the developer wanted more. The handler is where the lookups get awaited and the token gets signed, and that glue is exactly where a refactor can quietly swap two lines. So at intent review the developer chose to give the login handler its own async spec, so the proof covers that glue too instead of trusting it (Figure 6).

Comparison of two approaches to verifying a login handler. Option A, recommended, keeps the login handler trusted and verifies only the login decision logic, while database lookups and token signing remain tested but trusted components. Option B gives the login handler its own formal specification, proving the orchestration logic but requiring additional trusted wrappers and assumed contracts around database lookups, clocks, and JWT signing.

Figure 6. Proving more of login means trusting more small wrappers. The card says so before the developer chooses.

That choice moved almost everything into the proof and left the login handler with nothing to do but unpack the request. A new async function, login_flow, carries the contract from the request’s fields all the way to the token or the HTTP error. PDD Kit then brought the revised plan back for a fresh sign-off, because matching your intent isn’t proof, and it doesn’t approve new trust or a wider scope.

Step 4: Where proof stops

There are plenty of things you can’t prove about a program, and login runs into most of them:

  • Code the verifier can’t see. bcrypt’s hashing, sqlx’s queries and jsonwebtoken’s signing live in other crates, outside the proof.
  • Facts about the outside world. What SQLite holds right now and what time the clock says are inputs to the program, not something its code can establish.
  • Whether, and how fast, something finishes. The spec says what you get if login answers. A hung query, or a slow one, breaks nothing in it.
  • Anything a person has to judge, like whether an error message helps or a screen makes sense.
  • Whatever nobody wrote down. A proof covers its spec and says nothing about requirements outside it.

That’s where empirical verification comes in. PDD Kit spells out the trusted boundary, the part of the TCB this proof leans on. It wraps each trusted piece in a small function with an assumed contract, using Verus’s #[verifier::external_body] or external_fn_specification, asks the developer to accept each one on its own, and writes a test for every assumed clause into the plan. For login, the developer signed off on six pieces:

  • The two lookups, Customer::find_by_email and Admin::find_by_username. Each awaited, successful lookup returns exactly the row its own table holds for that email or username, or none.
  • check_password, a thin wrapper around bcrypt::verify. It errors only on a stored hash bcrypt can’t read, and Ok(true) means exactly the right password.
  • generate_token, which signs exactly the claims it is given under the configured secret and reports a signing failure as a 500.
  • expiry_from_now, which reads the clock and writes an expiry exactly 24 hours later.
  • database_error, which turns a failed lookup into today’s 500 with sqlx’s message.
  • The login handler shell, which unpacks the request and passes its fields to login_flow unchanged.

In the spec, a lookup keeps its code where it is and gets a contract from the outside with external_fn_specification:

/// Looks up a customer by exact email.  /// Trusted, not proved: the SQL query runs in sqlx, which Verus cannot check.  /// Every customer promise of `login_flow` rests on it.  #[verifier::external_fn_specification]  pub async fn ex_customer_find_by_email(pool: &sqlx::SqlitePool, email: &str)      -> (r: Result, sqlx::Error>)      ensures          // When the query succeeds, it returns the customers row on          // file for exactly this email, or None when there is none.          r matches Ok(found) ==> found == customer_on_file(email@),  {      Customer::find_by_email(pool, email).await  }

The clock is a small wrapper marked #[verifier::external_body]. Verus uses its ensures clause and never reads the body:

/// Reads the clock and computes a token's expiry 24 hours ahead.  /// Trusted, not proved: chrono's clock is outside the proof.  /// It panics if the time is out of range, as today's login does;  /// that panic is not promised against.  #[verifier::external_body]  pub fn expiry_from_now() -> (r: Expiry)      ensures          // The expiry is exactly 24 hours (86,400 seconds) after the clock reading.          r.exp as int == r.now as int + 86_400,  {      ...  }

Figure 7 puts the three layers together: what Verus proves, what people accept and tests check, and what nobody is promising.

Diagram showing the proof boundary for a formally verified login system. Functionality outside the proof is either not promised or trusted and validated through human review and testing, including SQLite lookups, bcrypt, the system clock, JWT signing, database error handling, and request parsing. At the center, the formally verified portion consists of the login_flow and its components login_core, lookup_target, decide_login, and issue_token, which are proven by Verus for every input.

Figure 7. Where the login proof stops. Verus proves the core for every input. People accept the trusted ring and tests check it. The plan lists what nobody is promising.

Figure 8 maps what gets proved and what gets trusted. Each trust decision is its own card. It shows the clauses Verus will take on faith, what the developer is accepting, and which promise falls apart if the assumption is wrong (Figure 9). Every assumed clause then gets an empirical check in the plan’s Empirical verification section, written as Given/When/Then scenarios before any test exists (Figure 10). An entry says planned until it has run, and then it records the command’s result and the commit it ran against.

Proof map showing how a verified login system is divided between formally proved code and trusted, tested components. Five core functions, including login_flow, login_core, lookup_target, decide_login, and issue_token, are formally verified with Verus. Supporting functions for database queries, password verification, token signing, clock access, and error handling are treated as trusted contracts that are reviewed and tested.

Figure 8. What gets proved and what gets trusted in login. Green functions are proved by Verus for every input. Amber pieces are trusted, accepted by the developer, and checked by the planned tests in the plan’s §4.1.

Proof map showing how a verified login system is divided between formally proved code and trusted, tested components. Five core functions, including login_flow, login_core, lookup_target, decide_login, and issue_token, are formally verified with Verus. Supporting functions for database queries, password verification, token signing, clock access, and error handling are treated as trusted contracts that are reviewed and tested.

Figure 9. A trust card for check_password. Verus takes these two clauses on faith, so a person accepts them and two tests check them.

Screenshot of a test plan for verifying password validation behavior. The section, “check_password_answers_with_bcrypts_verdict,” describes unit tests confirming that password checks return true for the correct password and false for incorrect or empty passwords. Example test cases use bcrypt hashes for customer and administrator accounts.

Figure 10. A planned test for the trusted check_password, written before the test exists. The plan labels a passing test as evidence rather than proof.

The plan is just as explicit about what nobody is promising: that the login future ever completes, that an out-of-range clock won’t panic, anything about the timing or rate of attempts, and how other endpoints read the role.

Step 5: Reading the spec

The intent artifact is what the team reviews. It’s the plain-language version of the formal spec PDD Kit writes as Verus contracts. You might be wondering whether someone now has to read the proof instead of the code. Nobody does, because the coding agent writes the proof and Verus checks every step of it. If Verus accepts the proof, it holds whether or not anyone reads it. The spec is what the proof is held to, so that’s what people review, and this one is short enough to learn to read in a few minutes. Each function gets an ensures block: statements about its result, r, that must hold for every possible input. Verus checks them when you build, and if any clause could fail for even one input, the build fails and Verus points at that clause. Each clause reads as an if-then, and Figure 11 takes apart the clause for the developer’s first requirement.

Annotated code snippet explaining a formal verification clause for login tokens. The clause states that if a login returns a token, then the request names a valid account, the password matches the stored hash, the token contains valid claims, and the token’s subject and role match the authenticated account. Numbered callouts explain each part of the expression.

Figure 11. The ensures clause on login_flow for “You only get a token with the right password.” The same account a appears on every line, which ties the password check to the token.

Notice that the same a appears on every line, which ties the password check to the token. For example, imagine a bug where login checks the password against one account but signs the token for another. Bob logs in with his own, correct password and gets back a token for Alice’s account. Each step looks fine on its own, but this clause catches it, because the password check and the token have to name the same account. That’s the kind of wiring bug a test suite rarely thinks to look for. The spec also has to talk about SQLite, bcrypt and JWTs without running any of them. It does that with uninterpreted functions, names with no body: customer_on_file(email) and admin_on_file(username) for the rows the database holds, bcrypt_verdict(password, hash) for what bcrypt would say, and token_claims(token, secret) for what a token carries. Verus knows nothing about them except what the trusted contracts from Step 4 promise. The proof says “whatever the database holds, login does the right thing with it,” and a trusted contract says “the lookup returns what the database holds.”

/// The customers row the database holds for  /// exactly this email, if any, when login looks.  /// Left abstract: only `Customer::find_by_email`'s  /// trusted contract says what it is.  pub uninterp spec fn customer_on_file(email: Seq) -> Option;  ...  /// What bcrypt says about `password` against a  /// stored `hash`: Some(true) when it is the password  /// the hash was made from, Some(false) when it is  /// not, None when bcrypt cannot read the hash.  /// Left abstract: its meaning is bcrypt's, taken on trust through `check_password`.  pub uninterp spec fn bcrypt_verdict(password: Seq, hash: Seq)      -> Option;

/// The password is the right one for the stored hash, by bcrypt's verdict.  pub open spec fn password_matches(password: Seq

, hash: Seq) -> bool {      bcrypt_verdict(password, hash) == Some(true)  }

On top of those, named_account(email, username) says which account a login names: the customers row for the email when one is sent, otherwise the admins row for the username, otherwise none. It’s written like Rust, but it’s a mathematical definition that never runs and only appears in contracts. Code like this is called ghost code, since the verifier checks it but the compiler erases it from the program. With it, the developer’s two-sentence request ends up as eight ensures clauses on login_flow (Figure 12).

/// The account a login names: the customers row for  /// the email when one is sent (role "customer"),  /// otherwise the admins row for the username (role "admin"), otherwise none.  pub open spec fn named_account(email: Option, username: Option)      -> Option {      match email {          Some(e) => match customer_on_file(e@) {              Some(c) => Some(NamedAccount {                  id: c.id@, password_hash: c.password_hash@, role: customer_role() }),              None => None,          },          None => match username {              Some(u) => match admin_on_file(u@) {                  Some(a) => Some(NamedAccount {                      id: a.id@, password_hash: a.password_hash@, role: admin_role() }),                  None => None,              },              None => None,          },      }  }

Traceability diagram showing how formal verification promises are derived from requirements. Statements made by developers and decisions recorded in design cards are mapped to eight specific guarantees in the login_flow specification, covering token issuance, account roles, invalid credentials, missing accounts, malformed requests, and error handling.

Figure 12. The two-sentence request and the Both ways decision ended up as eight ensures clauses on login_flow. Four of them keep today’s refusals, so the storefront and the existing API tests keep working.

Here’s the whole contract, with its doc comment trimmed. Each clause has the plain-English comment above it that the intent artifact shows the team:

/// The whole login, awaited end to end: from the  /// request's fields to the token or the HTTP error.  /// ...  pub async fn login_flow(pool: &sqlx::SqlitePool, config: &Config,      email: Option, username: Option, password: &str)      -> (r: Result)      ensures          // Neither an email nor a username: 400 with today's message.          email is None && username is None              ==> (r matches Err(AppError::BadRequest(m))                  && m@ == missing_identity_text()),          // A token only for the account the login names, with its right          // password; it carries that account's id and its table's role.          r matches Ok(t) ==> (named_account(email, username) matches Some(a)              && password_matches(password@, a.password_hash)              && token_claims(t@, config.jwt_secret@) matches Some(c)              && c.sub@ == a.id && c.role@ == a.role),          // The right password for the named account always gets a          // token, unless a lookup or signing failure gives 500.          (named_account(email, username) matches Some(a)              && password_matches(password@, a.password_hash))              ==> (r is Ok || r matches Err(AppError::Internal(_))),          // A login naming an account nobody holds gets          // 401, unless the lookup fails with 500.          ((email is Some || username is Some) && named_account(email, username) is None)              ==> (r matches Err(AppError::Unauthorized)                  || r matches Err(AppError::Internal(_))),          // A wrong password for an account whose hash bcrypt          // can read gets 401, unless the lookup fails with 500.          (named_account(email, username) matches Some(a)              && bcrypt_verdict(password@, a.password_hash) == Some(false))              ==> (r matches Err(AppError::Unauthorized)                  || r matches Err(AppError::Internal(_))),          // An account whose stored hash bcrypt cannot read gets 500 and no token.          (named_account(email, username) matches Some(a)              && bcrypt_verdict(password@, a.password_hash) is None)              ==> r matches Err(AppError::Internal(_)),          // The admin role only for an admins row          // found by username, with no email sent.          (r matches Ok(t) && token_claims(t@, config.jwt_secret@) matches Some(c)              && c.role@ == admin_role())              ==> (email is None && username matches Some(u)                  && admin_on_file(u@) is Some),          // An email login's token always carries the customer role.          (r matches Ok(t) && email is Some)              ==> (token_claims(t@, config.jwt_secret@) matches Some(c)                  && c.role@ == customer_role()),  {      unimplemented!()  }

The trusted boundary shows up in the code too. check_password is one of the functions whose body Verus doesn’t check. Its contract ties it to bcrypt_verdict, an uninterpreted model of bcrypt with no axioms:

/// Asks bcrypt whether `password` matches the stored `hash`.  /// Trusted, not proved: Verus cannot check bcrypt's hashing,  /// so this wrapper's contract is assumed.  /// Every password promise of `login_flow` rests on it.  #[verifier::external_body]  pub fn check_password(password: &str, hash: &str) -> (r: Result)      ensures          // bcrypt answers (Ok) exactly when it can read the stored hash.          r is Ok <==> bcrypt_verdict(password@, hash@) is Some,          // Its answer is its verdict: true for the          // right password, false for any other.          r matches Ok(b) ==> bcrypt_verdict(password@, hash@) == Some(b),  {      bcrypt::verify(password, hash).map_err(|e| e.to_string())  }

The doc comment above it says what rests on it: every password promise of login_flow. Every login ends in one of a handful of places, and only one of them has a token (Figure 13). The proof covers all of them at once.

Flowchart showing the end-to-end login process. A login request selects an account table based on an email or username, looks up the account, verifies the password, and issues a signed token when authentication succeeds. Error paths return appropriate HTTP responses: 400 for missing credentials, 401 for invalid credentials or missing accounts, and 500 for lookup or password-hash failures.

Figure 13. Every way a login can end, from the plan.

login_flow is async: it awaits a database query. Verus can put contracts on async functions, with one rule worth knowing (Figure 14). requires is checked when the call creates the future. ensures describes the value you get when you await it. Nothing promises the future finishes, so a hung query breaks no clause.

The doc comment on login_flow says so in as many words:

/// ... No promise the future completes.  pub async fn login_flow(...) -> (r: Result)

Diagram comparing how formal verification applies to synchronous and asynchronous functions. For both types, preconditions are checked when the function is called and postconditions are guaranteed when a result is returned. In an asynchronous login flow, the function may pause while awaiting a database lookup, and the specification makes no guarantee about whether or how quickly the operation completes.

Figure 14. When a contract is checked, for a sync fn and an async fn.

login_flow stays short because it’s built from smaller proved pieces. lookup_target picks the table. login_core awaits the lookup and calls decide_login, the password gate. issue_token signs the token, or maps a refusal to today’s HTTP error. Each piece has its own contract, and Verus proves login_flow using only those contracts, without re-reading their bodies. Change the body of decide_login and only decide_login gets proved again.

Here’s the smallest piece, whole, and the start and end of the password gate:

pub fn lookup_target(email: Option, username: Option)      -> (r: Result)      ensures          // An email, when sent, wins: customers are searched for exactly that email.          email matches Some(e)              ==> r == Ok::(LookupTarget::CustomerByEmail(e)),          // With no email, a username sends the search          // to admins for exactly that username.          email is None && username matches Some(u)              ==> r == Ok::(LookupTarget::AdminByUsername(u)),          // With neither, the login is refused for missing an identity.          email is None && username is None              ==> r == Err::(LoginError::MissingIdentity),

pub fn decide_login(target: &LookupTarget, found: Option, password: &str)      -> (r: Result)      ensures          // No row found: refused as Unauthorized,          // the same answer as a wrong password.          found is None              ==> r == Err::(LoginError::Unauthorized),          // Claims come back only for a found row          // whose stored hash matches the password.          r is Ok ==> found is Some              && password_matches(password@, found->Some_0.password_hash@),          // The right password for a found row always gets claims.          found matches Some(a) && password_matches(password@, a.password_hash@)              ==> r is Ok,          ...          // An admins-table search gives the role text "admin".          r matches Ok(c) && target is AdminByUsername ==> c.role@ == admin_role(),          // A customers-table search gives the role text "customer".          r matches Ok(c) && target is CustomerByEmail ==> c.role@ == customer_role(),

An overview of login.rs

Figure 15 maps the whole spec file, verified/src/login.rs. It reads top to bottom in four layers: headers, imports and types Verus needs to know about, an abstract view of the outside world, the trusted contracts that connect that view to real code, and the five contracts the proof has to meet.

Annotated map of the verified/src/login.rs file showing the structure of a formally verified login implementation. The diagram categorizes sections as specification-only definitions, trusted external contracts, data types, and five functions formally proved with Verus: login_flow, login_core, issue_token, lookup_target, and decide_login.

Figure 15. The parts of login.rs, in file order. Gray is header and types, purple is math that only appears in contracts, amber is trusted, and green is proven.

Here are two of the less obvious parts, abridged. external_type_specification introduces a type Verus didn’t define. The pool stays opaque, while a customers row keeps its fields visible:

/// The connection pool, opaque to the proof: only  /// the lookups' contracts say what it returns.  #[verifier::external_type_specification]  #[verifier::external_body]  #[verifier::reject_recursive_types(DB)]  pub struct ExPool(sqlx::Pool);  ...  /// A customers row, with its fields visible to the proof.  #[verifier::external_type_specification]  pub struct ExCustomer(Customer);

And LoginError keeps the decision separate from the HTTP response, which is what lets issue_token pin each refusal to today’s status code:

/// Why a login gets no token.  pub enum LoginError {      /// Neither an email nor a username was sent: 400 Bad Request.      MissingIdentity,      /// No such account, or the password does not match: 401 Unauthorized.      Unauthorized,      /// bcrypt could not read the stored hash, with its message: 500 Internal.      UnreadableHash(String),      /// The lookup query failed: 500 Internal, with sqlx's message.      Database(sqlx::Error),  }

Before the plan comes back to you, two reviewer agents poke holes in the derived formal spec from the intent artifact to ensure a high-quality translation, so that the spec matches what you asked for and doesn’t promise more than a proof could show. The spec refinement runs in the background and takes live user feedback, iterating until reviewers agree that the spec is sound.

Step 6: Prove, check, close the gaps

Signing off on the spec is where the developer’s review ends. Everything after it, the implementation and the proof, is checked by machines, which is why this post spends its time on the spec and not the proof. Once the developer signs off, a Build plan stage (Figure 16) hands the approved plan to pdd-prove, which turns it into a small task graph (Figure 16). For login, that’s seven tasks: get login.rs building under Verus inside the backend crate, prove the decision, the token and the whole flow, then write the tests for the trusted wrappers and the end-to-end login.

Screenshot of a PDD Kit build plan for a login-token issuance specification. The build workflow contains seven tasks, with the first task focused on making verified/src/login.rs successfully build under Verus while preserving contract meanings. Subsequent tasks cover proving login-related functions, testing trusted wrappers, running end-to-end login tests, and validating token-signing behavior.

Figure 16. Build plan turns the approved spec into seven tasks on a development machine. The first gets login.rs building under Verus without changing what any contract promises, and stops to ask rather than weaken a contract.

Each task runs the same loop:

  • A coding agent writes the implementation and the proof, and Verus checks them against the derived formal spec.
  • Anything in the trusted boundary gets the tests the plan called for.
  • An LLM verifier reads the plan, the specification and the results side by side, and looks for gaps: requirements with no proof and no passing check. Gaps go back to the coding agent until none remain. If the specification itself has a gap, such as two requirements that contradict each other, it comes back to the developer as a decision.

A proof is only as good as its spec, so pdd-prove checks the spec too. When a model writes its own spec, or is tuned to produce proofs, it tends to land on weak invariants: clauses that verify but rule out almost nothing. They prove something, just rarely the thing you care about. We call that “vibe verification.” That’s why it’s so important to make sure your intent is aligned with the agent and what PDD Kit is great for.

The loop is strict about not moving the goalposts. If an async construct can’t be expressed in the pinned Verus, the task stops and asks the team instead of switching to a non-async helper or weakening a contract.

How does PDD Kit remove classes of bugs? The mutation check is how PDD Kit guards against vibe verification. pdd-prove plants one bug per ensures clause in a scratch copy of the code, like refusing everyone or checking the username before the email, and Verus has to reject each mutant on the clause it targets. A mutant that gets through means that clause constrains nothing, and it comes back as a gap.

Step 7: Review intent alongside verified code

When the tasks pass, pdd-prove opens pull requests. They add a verified/ crate with login_flow, its pieces, their proofs, and the trusted wrappers with their tests, and route the real login handler through login_flow, keeping the existing HTTP status codes. Your verified logic integrates with and hardens your production login path, not a copy of it.

The developer was able to merge without relying on exhaustive line-by-line review of the generated code. The review had already happened on the intent artifact this whole time, where the team has the final say. We shifted more of the review focus from code review to intent review. The pull request works as a receipt, and its description marks every login requirement as either formally verified or empirically checked.

Merging is where the proof joins the team’s normal workflow. PDD Kit’s pdd-ship workflow adds the Verus check to CI next to the empirical tests, so every later change to login, by a person or an agent, has to keep proving against the same spec, and a broken proof fails the build like a failing test. And because a proof covers every input, PDD Kit prunes tests that only re-check what’s already proved, such as example-based tests of the password gate, and keeps the ones that guard the trusted boundary, so CI gets stronger and leaner at the same time.

What the proof does and doesn’t say

“Fully verified” is easy to oversell, so here’s exactly what we’re claiming.

  • Proved. The bodies of login_flow, login_core, lookup_target, decide_login and issue_token meet their contracts for every input. Login picks the right table, compares the request’s password against the stored hash of the account it names, issues a token with that account’s id and the role its table gives, and refuses everything else with today’s 400, 401 or 500 and no token.
  • Trusted and tested. The two database lookups, check_password over bcrypt, the clock, token signing, the database error message and the handler’s request unpacking sit outside the proof, each behind an assumed contract the developer accepted. Tests against the real bcrypt, the real SQLite schema and the real router cover them.
  • Assumed. The Verus, Rust compiler and Z3 (opens in new tab) toolchain, and Verus’s standard library vstd (opens in new tab).

Why Rust

PDD Kit only verifies Rust today, since that’s the language Verus works on. If your codebase is already Rust, there’s nothing to migrate. Code in other languages can be ported to Rust and checked against the same spec, and non-Rust projects can call verified Rust through bindings.

There’s also a less obvious upside. Once reliance on exhaustive line-by-line review has been reduced, readability stops being the main reason to pick a language. You can pick one for speed and safety, and optimize as hard as you want, because any change that breaks the spec fails the proof.

What to Expect from PDD Kit

PDD Kit is a research prototype, and it only verifies Rust today since that’s the language Verus works on. Code in other languages can be ported to Rust and checked against the same spec, and non-Rust projects can call verified Rust through bindings. Proofs still take real effort, mostly in producing the proof itself, and a clear spec is what keeps that effort down, which is why so much of PDD Kit is about getting intent right first. We’d start where a bug costs the most, such as login, payments, or permissions, and verify the rest of a codebase one piece at a time.

Pet Store’s login is one piece of one app: a spec anyone can read, a Verus proof that the code matches it, and tests for everything the proof takes on trust. We want to be upfront about the weakest links. The guarantees are only as strong as the six trusted pieces, and two carry most of the weight: check_password for every password promise, and the two lookups for every promise about which account a token names. Tests support them, but nothing proves them. We expect that trusted boundary to shrink significantly over time, so a growing share of the code can be formally verified, and we want to find out how far this scales.

What we’re really changing is what people review. Instead of relying mainly on line-by-line review of agent-generated code, a team reviews what it meant, in plain language, and a proof holds the code to it. The plan was always supposed to be the contract, and now the agent actually has to keep it.

Acknowledgments

This work is a collaboration between the Microsoft Research Catalyst Lab, GitHub Next, and the Research in Software Engineering (RiSE) group. We would like to thank Maggie Appleton and Krzysztof Cieślak of GitHub Next for their collaboration on Chopin, and Nikhil Swamy, Lef Ioannidis, Janardhan Kulkarni, and Shuvendu Lahiri for their feedback and discussions on formal verification and intent formalization. We also thank the F*/Pulse and Verus teams for their support. Finally, we thank Idan Gazit of GitHub Next for supporting this collaboration.

FAQ

  • You do, and yes, it would. A proof shows the code meets the spec, not that the spec is what you meant. That’s why PDD Kit is built around existing research in intent formalization. The pdd-plan workflow reads your code, asks questions before writing anything, and turns your answers into a Verus spec plus a plain-language plan you approve, flagging ambiguity instead of guessing. Two reviewer agents check the spec, and the pdd-prove workflow plants one bug per clause to catch clauses that verify but rule out nothing. A wrong spec is still possible, which is why PDD Kit emphasizes making requirements explicit, identifying ambiguities, and keeping developers involved in reviewing and approving the spec before verification begins.

  • The difference is what holds the code to the plan.

    Approach

    What checks the code

    Spec-Driven Development

    Nothing enforces the spec

    Test-Driven Development

    Tests on a sample of inputs, often written to match what was built

    Agent checks its own work

    The same model that wrote the code

    Proof-Driven Development

    Verus automatically verifies the proof that your code satisfies the spec your approved

    PDD keeps the planning loop from SDD and still uses tests where a proof can’t reach. A test checks some inputs, while a proof establishes that the code satisfies specified properties for all inputs covered by the formal model, under its stated assumptions.

  • The proof is Rust code annotated with Verus constructs, checked mechanically by Verus using the Z3 solver. It runs wherever your Rust builds: the pdd-prove workflow runs it while proving, and the pdd-ship workflow adds it to your build and CI so a broken proof fails the build. The check is deterministic, with no model involved. Models write the spec, code, and proof, but only a deterministic Verus run decides whether it passed. How long the check takes varies with the size of the code and how hard the proofs are: usually seconds for a small module, up to minutes for a larger codebase.

  • PDD Kit shifts more of the review process toward validating what the code should do, while using formal verification to provide additional assurance that the implementation meets those requirements. Developers review the natural-language plan, the formal spec, and the trusted components that fall outside the scope of the proof. The resulting pull request documents what was formally verified, what was tested, and where additional review may be needed. On the Pet Store full stack web app, this approach reduced reliance on exhaustive line-by-line review of the generated code.

  • We chose Verus because it allows us to verify Rust code directly, aligning with PDD Kit’s focus on integrating formal verification into existing Rust development workflows. Other proof languages (e.g., Lean, F*, Rocq) take different approaches to formal verification, each with its own tradeoffs. The techniques behind PDD Kit could help put them to work as well.

原文版权归 Microsoft Research 及原作者所有,本站转载仅供阅读。如需删除,请联系 support@aisa.one。