# Erratum — draft-fossil-sync-protocol-00 §6.6

| | |
|---|---|
| **Document** | `draft-fossil-sync-protocol-00` |
| **Section** | 6.6 — Capability Model (server refusal of `push` without Write); cross-references §8.5, §10.1 and §10.2 |
| **Type** | Technical (the server's refusal form changes: a `message` card, not an `error` card, when a pull is present) |
| **Status** | Proposed — operator decision pending |
| **Reported** | 2026-10-11 |
| **Submitter** | Dan Mestas |
| **Found via** | Reading §10.2 against the server side; reproduced on fossil 2.28 [52445a27f1] with `fossil http` and an `application/x-fossil-debug` request |

## 1. Summary

§10.2 tells a client what to do when it receives `message pull only *`: turn
push off for the session and hide the message. Nothing in the draft tells a
server to send it. §6.6 says instead that "the server checks each card's
required capability and emits error when lacking", and §10.1 lists
"authorization" among the reset sites. A server built from the draft
answers a `push` from a principal without Write with an `error` card.

Fossil 2.28 does that only when no `pull` card preceded the `push` in the
request. When one did, it emits the nonfatal
`message pull\sonly\s-\snot\sauthorized\sto\spush`, does not reset the
response, and serves the pull. Fossil's own client sends `pull` before
`push` in every round, so a stock `fossil sync` by a read-only user always
gets the message, never the error. In neither form does the push refusal
end card processing.

## 2. Location

- §6.6 (~lines 1189-1193): "The server checks each card's required
  capability and emits error when lacking, except where an owning section
  specifies silent behavior."
- §8.5 (~lines 1630-1634): the push/pull paragraph, which states no
  capability rule.
- §10.1 (~lines 2093-2097): "At wrong-project, authorization, duplicate-login,
  unknown-card, and other fatal reset sites, server discards partial output
  before error".
- §10.2 (~lines 2133-2138): the client's `pull only *` rule, unchanged.

## 3. The divergence

Fossil 2.28, `src/xfer.c:1503-1524`, the `push`/`pull` handler after the
project-code check:

```c
if( blob_eq(&xfer.aToken[0], "pull") ){
  if( !g.perm.Read ){ cgi_reset_content(); @ error not\sauthorized\sto\sread...
                      nErr++; break; }
  isPull = 1;
}else{
  if( !g.perm.Write ){
    if( !isPull ){
      cgi_reset_content();
      @ error not\sauthorized\sto\swrite%s(whyNotAuth())
      nErr++;
    }else{
      @ message pull\sonly\s-\snot\sauthorized\sto\spush%s(whyNotAuth())
    }
  }else{
    isPush = 1;
  }
}
```

Neither push branch executes `break`: card processing continues, and
`isPush` stays clear, so later `file`/`cfile` cards are refused
individually (`xfer.c:1361-1363`, `1382-1384`). The client emits `pull`
before `push` (`xfer.c:2475-2480`). `whyNotAuth()` appends a suffix only in
specific configurations; it was empty in these tests.

Observed with `fossil http`, `nobody` holding `o` only:

| Request operation cards | Reply |
|---|---|
| `pull`, `push` | `message pull\sonly\s-\snot\sauthorized\sto\spush`, the `igot` inventory, `# timestamp ... errors 0` |
| `push`, `pull` | `error not\sauthorized\sto\swrite`, the `igot` inventory, `# timestamp ... errors 1` |
| `push` | `error not\sauthorized\sto\swrite`, `# timestamp ... errors 1` |

The second row shows that the error form also does not end card
processing: the `pull` that follows it still produces the inventory.

## 4. Impact

Wire-visible. A server that answers a read-only user's `sync` with an
`error` card makes a stock Fossil client stop its reply loop on every round
(§10.1) and count an error, where against Fossil the same client
quietly switches to pull-only and completes. (The client-side effect is
taken from the draft's §10.1 and §10.2; only the server replies were
reproduced.) Post-commit autopush hides
part of this through the `%not authorized to write%` exception (§7.6), but
`fossil sync` and `fossil update` with autosync do not. A server that
treats the error as ending card processing also drops the pull's inventory
when `push` precedes `pull`.

## 5. Correction

In §6.6, after the sentence ending "specifies silent behavior.", add:

```diff
+   A push card from a principal without Write is refused in one of two
+   forms, and neither ends card processing.  If a pull card was already
+   accepted earlier in the same request, the server MUST NOT reset
+   accumulated output and MUST emit the nonfatal message
+   "pull only - not authorized to push" (fossilized:
+   message pull\sonly\s-\snot\sauthorized\sto\spush), which a client
+   handles as Section 10.2 states.  Otherwise the server resets
+   accumulated output and emits error not authorized to write.  In both
+   forms push is not enabled for the request, so later file and cfile
+   cards are refused individually, and later cards, including a pull,
+   are still processed.  A client that sends both cards SHOULD send
+   pull before push, as the reference client does, so that a refused
+   push yields the message form.
```

In §10.1, amend the reset-site sentence:

```diff
-   At wrong-project, authorization, duplicate-login, unknown-card, and
-   other fatal reset sites, server discards partial output before error;
+   At wrong-project, authorization, duplicate-login, unknown-card, and
+   other reset sites, server discards partial output before error (the
+   push refusal of Section 6.6 has a non-reset message form, and it and
+   the unknown-card error continue card processing);
```

## 6. Rationale

- **Gives §10.2 its producer.** The draft specifies the client half of the
  pull-only exchange; this specifies the server half, so the pair
  interoperates with Fossil in both directions.
- **States the order dependency.** The message form depends on a `pull`
  having been seen first. Stating it lets a cleanroom server match Fossil
  for the request order real clients use, and for the reverse order too.
  §3.2.1 fixes no order between `pull` and `push`, so the SHOULD names
  the order the reference client uses (`xfer.c:2475-2480`).
- **Keeps the error text.** The error form and its text are the ones the
  autopush exception in §7.6 already matches, so nothing else changes.

## 7. Verification

- Fossil 2.28 [52445a27f1], macOS arm64, `fossil http REPO` with an
  `application/x-fossil-debug` POST to `/xfer`, the three request shapes
  in the table above, `nobody` capabilities `o`.
- Source references are to `src/xfer.c` at tag `version-2.28`.
- go-libfossil's client cases `sync.10.2.pull-only` and
  `sync.10.2.pull-only-gimme` exercise the client half only.

---

*Prepared against `draft-fossil-sync-protocol-00`. The RFC remains the oracle; the reference implementation is used only to confirm the observable behavior.*
