Notarizing documentation

Collaboration without a hosted mode

Developer preview. Not yet production ready. This page is rendered from docs/collaboration.md of the Notarizing repository at revision d23579e737f9d1e16e07b3c6b21a51282d64027e. It describes the behavior of that revision.

Contents

Notarizing has no shared server and no user accounts. Each reviewer owns a workspace on their own machine. Reviewers share work as files:

FileCommandWhat it carries
Evidence bundle (notarizing.bundle/1)bundle export, bundle importReceipts with the exact report and artifact bytes, corrections and, with --target, the review events of the target as attributed claims.
Review export (Markdown or JSON)review exportThe review of one change at one pinned checkpoint: review states, notes, wording drafts, diagnostics and limits.
Review patchreview patchOne wording draft as a proposal-only patch at an exact base commit.

None of these files changes the review state, the policy or the bindings of the receiving workspace. Each workspace keeps its own decisions.

Worked example

This example ran on 2026-10-02 with the binary of this revision. Alice owns the workspace of the refund limits demonstration (make demo-refund-limits). Bob starts an empty workspace. Commit IDs, receipt IDs, bundle IDs and session IDs differ in each run.

1. Alice records review decisions

Alice runs notarizing serve, opens the browser and asks for a review grant:

notarizing review session grant --session ses_ID --ttl 30m --scope change:demo-payments/add-refund-limits

In the browser she accepts the assumption ASM-UTC-DAY for the scope and writes a wording draft for PAY-R-104. (For this run, a script sent the same two review commands to the browser API with the session cookie and the CSRF token.) The journal then has two events by browser session 1413 at checkpoint 2.

Without --actor, the actor label of an event names a browser session, not a person. To name the reviewer, add --actor to the grant:

notarizing review session grant --session ses_ID --ttl 30m --scope change:demo-payments/add-refund-limits --actor "Alice Example"

The events of that grant then have the actor label Alice Example (unverified, browser session 1413). The review export, the journal and a bundle show this text. The label is a claim that the operator typed. Notarizing does not verify it, and it changes nothing that the grant allows. The label has 1 to 28 letters, digits, single spaces or the characters . , ' - _ @.

2. Alice exports the review, the patch and the evidence

notarizing review export --repo demo-payments --change add-refund-limits --checkpoint latest --output review.md
notarizing review patch --draft DRAFT_EVENT_ID --base FULL_SHA --output change.patch
notarizing bundle export --target tgt_ID --output evidence.bundle
notarizing bundle export --source local --output all.bundle

The output of this run:

wrote review.md (markdown, 11251 bytes, sha256 261e6f0d...)
target tgt_xxwrnsvhrkxqhdz6rns24j4ktb at b7719d84d43e, review checkpoint 2
wrote change.patch (sha256 ...)
wrote change.patch.manifest.json
status: proposal_only. Check the patch against your working tree; notarizing applied nothing.
wrote bundle bdl_g2yrryxiz6ntvfbrmarwziufsq to evidence.bundle: 1 receipts, 4 files, 9830 bytes, complete false
wrote bundle bdl_5abwkc2olebpgmyqlw4deey555 to all.bundle: 4 receipts, 7 files, 18301 bytes, complete false
omission: {"kind":"review_events_not_exported","id":"","reason":"Review events belong to a target. The selection names no target."}
  • --target selects the receipts about the repository and commit of the target and adds the review events of the target. Here it selected 1 receipt: the other 3 reports are about revision 1.
  • --source local selects every receipt of the local source, but no review events.
  • complete false is normal: a bundle never carries the source documents.

review.md names each decision with its actor, time, sequence and rationale, for example "accepted_for_scope by browser session 1413 ... sequence 1: All services log in UTC".

3. Bob builds the same target

Bob needs the same repository content and the same change at the same commit. He clones the repository and imports the change at the full commit ID:

git clone ALICE_REPO_URL demo-payments
notarizing init
notarizing repo add demo-payments ./demo-payments
notarizing change import --repo demo-payments --at FULL_SHA --change add-refund-limits
notarizing binding import bindings.json

The same content gives the same target ID (tgt_xxwrnsvhrkxqhdz6rns24j4ktb in both workspaces). The bundle import reports this as "local":"same".

4. Bob imports the bundles

notarizing bundle import all.bundle
notarizing bundle import evidence.bundle
bundle bdl_g2yrryxiz6ntvfbrmarwziufsq imported: 1 receipts, complete false
source: {"target_id":"tgt_xxwrnsvhrkxqhdz6rns24j4ktb",...,"local":"same"}
review events: 2 claims of the exporting workspace
notice: Imported review events are claims of the exporting workspace. They are not local review decisions, do not change a review state or a policy, and grant no review authority.

The import checks the archive, every digest and every report before it writes. It stores all receipts or nothing. The same bundle again returns the first import.

After the import, the change report of Bob shows the receipts, but no row counts:

row CHK-LIMIT-UNIT [any configuration]: reported pass (complete, 3 of 3 cases, imported_bundle via bundle) -> evaluated unknown (untrusted_source)

ASM-UTC-DAY is still open for Bob. Alice's decision is a claim. To agree, Bob records his own decision in his browser with his own grant.

Trust the evidence of a bundle

An imported receipt has the source namespace bundle:<bundle_id>:<hash>. The hash comes from the original namespace. Bob's trust of local does not apply to it. To let the receipts of one bundle count, Bob trusts that exact namespace. evidence show RECEIPT_ID shows it: attribution imported_bundle via bundle, source bundle:bdl_g2yrryxiz6ntvfbrmarwziufsq:ew7y4grdspyrbdjx.

{
  "schema_version": "notarizing.policy/1",
  "trusted_sources": [
    {"source_namespace": "bundle:bdl_g2yrryxiz6ntvfbrmarwziufsq:ew7y4grdspyrbdjx", "methods": ["implementation_test", "integration_test"]}
  ],
  "exclusions": []
}
notarizing policy set policy.json
row CHK-LIMIT-UNIT [any configuration]: reported pass (complete, 3 of 3 cases, imported_bundle via bundle) -> evaluated pass (none)

A bundle digest authenticates the bytes, not the exporter. Trust a bundle only when you know where it came from. Each new bundle has a new namespace, so a later bundle needs a new policy version.

Notarizing has no policy entry that trusts all later bundles of one source. A bundle is not signed and carries no identity of the exporting workspace. Any value that such an entry could match, for example the original namespace local, can be written into a file by anyone. The entry would trust bytes that you never checked. The new policy version for each bundle is the record that you checked this one bundle. For evidence that arrives again and again from one source, use the ka2a adapter: it binds a verified principal to a fixed source namespace, and one policy entry trusts it (see the operator guide).

5. Bob reviews the wording draft

Bob reads review.md and checks the patch against his working tree. Notarizing never applies it:

git -C demo-payments apply --check ../change.patch
git -C demo-payments apply ../change.patch

In this run the patch applied: 2 insertions and 2 deletions in openspec/changes/add-refund-limits/specs/refunds/spec.md. A changed base file gives a conflict at review patch, not a partial patch.

Rules

  • Exchange bundles, exports and patches over a channel that you trust. A digest proves that the bytes did not change. It does not prove who made them. See the threat model.
  • Import the change at the full commit ID. A branch name can point to another commit on another machine.
  • Keep one bindings file and one policy under version control, and import them in each workspace. A bundle carries neither.
  • Imported review events and corrections stay claims. Only a local reviewer with a local grant changes a local review state.

Known gaps

  • The CLI change report does not list imported review events. The browser inspector of an object shows them under "Imported review claims" (at most 50 for each object). The bundle import result (--output json) lists them all.
  • A review event names a person only as an unverified label that the operator gave with the grant (--actor). Notarizing has no user accounts and does not verify the label.
  • There is no merge of two review journals. Each workspace keeps its own journal.

All Notarizing documents