feat(IDEA-213): contracten voor managed queue dispatch (ST-1590) #61

Merged
janpeter merged 14 commits from feat/idea-213-dispatch into main 2026-09-21 19:07:36 +02:00
Owner

Draft — niet mergen. Geopend om CI te laten draaien; IP-10 t/m IP-14 van ST-1590 zijn nog niet uitgevoerd. Hoort bij Scrum4Me #251; de zeven feat/idea-213-dispatch-branches horen bij elkaar.

Inhoud

Gedeelde contracten (IP-01, IP-06 t/m IP-08): dispatch-types en job-kinds, statetabel, getekende start-permit, runtime-stopobservatie, bronpinning, validatie, gedeelde document-reader en de dispatch-modellen in prisma/schema.prisma.

Review-fixes (code-review 2026-09-20)

Geen wijzigingen in deze repo. Open MINORs: T-1853 (canonicalisatie, branch-ref, reply_to), T-1854 (klok-tolerantie en key-id van de start-permit), T-1855 (RUNNING bereikbaar zonder start).

Verificatie (lokaal)

  • Niet opnieuw gedraaid in deze sessie; de vendored kopie in scrum4me-mcp (95a4d8b) is door de reviewer gelijk bevonden aan deze HEAD.
  • Let op: Scrum4Me pint de submodule op 6d72c8f, twee commits vóór deze HEAD.

Commits

  • 365ac3f feat(IDEA-213): define managed dispatch contracts and job kinds
  • bd8e9fe feat(IDEA-213): define signed start permit contract
  • 6d72c8f feat(IDEA-213): expose guarded Task dispatch occupancy
  • 70bb86f feat(IDEA-213): define bound runtime stop observations
  • 3dd2b71 fix(IDEA-213): require successful exact-scope stop observation
  • 2f3ba33 feat(IDEA-213): pin execution sources and preserve code artifacts
  • 95a4d8b fix(IDEA-213): align managed source key and wire limits

🤖 Generated with Claude Code

Draft — niet mergen. Geopend om CI te laten draaien; IP-10 t/m IP-14 van ST-1590 zijn nog niet uitgevoerd. Hoort bij [Scrum4Me #251](https://git.jp-visser.nl/janpeter/Scrum4Me/pulls/251); de zeven `feat/idea-213-dispatch`-branches horen bij elkaar. ## Inhoud Gedeelde contracten (IP-01, IP-06 t/m IP-08): dispatch-types en job-kinds, statetabel, getekende start-permit, runtime-stopobservatie, bronpinning, validatie, gedeelde document-reader en de dispatch-modellen in `prisma/schema.prisma`. ## Review-fixes (code-review 2026-09-20) Geen wijzigingen in deze repo. Open MINORs: T-1853 (canonicalisatie, branch-ref, reply_to), T-1854 (klok-tolerantie en key-id van de start-permit), T-1855 (RUNNING bereikbaar zonder start). ## Verificatie (lokaal) - Niet opnieuw gedraaid in deze sessie; de vendored kopie in scrum4me-mcp (`95a4d8b`) is door de reviewer gelijk bevonden aan deze HEAD. - **Let op:** Scrum4Me pint de submodule op `6d72c8f`, twee commits vóór deze HEAD. ## Commits - `365ac3f` feat(IDEA-213): define managed dispatch contracts and job kinds - `bd8e9fe` feat(IDEA-213): define signed start permit contract - `6d72c8f` feat(IDEA-213): expose guarded Task dispatch occupancy - `70bb86f` feat(IDEA-213): define bound runtime stop observations - `3dd2b71` fix(IDEA-213): require successful exact-scope stop observation - `2f3ba33` feat(IDEA-213): pin execution sources and preserve code artifacts - `95a4d8b` fix(IDEA-213): align managed source key and wire limits 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(IDEA-213): define the dispatch projection contract for Messages
All checks were successful
CI / Verify (pull_request) Successful in 19s
5209199db0
A managed request appears in Messages as exactly one root and, once it
is terminal, exactly one reply. The projection is a complete snapshot at
its version, maps the managed state onto the five legacy statuses while
keeping the real state in meta.dispatch, and keeps the pinned review
documents as a sibling of meta.task on both messages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
canonicalPreparedSourcesManifest sorted source keys with localeCompare
(ICU/locale-dependent for '_', '-' and case) and derived the property
order implicitly from the zod shape, so a consumer that re-canonicalises
to verify the signed string could produce different bytes than the signer.
Switch to UTF-16 code-unit ordering ('<' on strings) and build the object
in an explicit property order, mirroring canonicalStartPermitClaims. Bound
repository.productId (<=128, no control/space chars) and enforce
DISPATCH_SOURCE_MANIFEST_MAX_BYTES at the production site. A fixture pins
both the old localeCompare form and the new code-unit form so the wire
change is visible and locked. Safe now because nothing is signed in
production yet (draft PRs); every consumer re-pins afterwards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(m9b) dispatchResultSchema.code.branch accepted free text up to 32k, so
values like `--upload-pack=x` or `refs/../x` could reach the central
publisher's git argv. Add a branchRef regex that accepts exactly the
`codex/queue-<uuid>` ref the mcp publisher already requires
(scrum4me-mcp src/dispatch/publication.ts:29; docker consumer
scrum4me-docker lib/dispatch-code-artifact.ts:78) and rejects spaces,
traversal and flag-like values.

(m9c) reply_to accepted any parseable queue address, including the
synthetic dispatch (scrum4me-dispatch:<uuid>) and job (scrum4us-job:<id>)
namespaces. reply_to is the SENDER's address; those namespaces are
outbound-only destinations. Restrict it to real agent/human addresses in
the schema (moving the rule out of the comment). This agrees with the
projection, which already refuses a dispatch-server sender.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(IDEA-213): bounded clock-skew tolerance for start-permit (m10)
All checks were successful
CI / Verify (pull_request) Successful in 20s
ee7fe1a136
validateStartPermitClaims rejected any permit with issued > nowMs, so a
broker clock running a few ms behind the DB clock would refuse a permit
whose attempt is already committed RUNNING centrally. Allow a bounded
future-skew of START_PERMIT_CLOCK_SKEW_MS (5000ms), mirroring the 5s
issuer lead the mcp HTTP assertion already permits
(scrum4me-mcp src/dispatch/assertions.ts:62). The 5s permit lifetime and
all identity bindings are unchanged; the permit version is not bumped and
no key id is added (documented-only, handled elsewhere).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(IDEA-213): forbid reaching RUNNING without a start in the state table
All checks were successful
CI / Verify (pull_request) Successful in 42s
9c3ce16e58
The dispatch state table let a never-started attempt reach RUNNING via
CLAIMED -> lease_lost -> UNCERTAIN -> resume_same_attempt -> RUNNING, never
passing `start` and so never the start permit (review m11, ST-1590.35). A flat
UNCERTAIN cannot carry whether the attempt had started, so split it: lease loss
from CLAIMED now yields UNCERTAIN_UNSTARTED and lease loss from RUNNING yields
UNCERTAIN. Only UNCERTAIN resumes straight to RUNNING; UNCERTAIN_UNSTARTED
resumes to CLAIMED and must pass `start` again. dispatchRootStatus maps the new
state to `claimed`; the matrix test pins the exact allowed transition set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(IDEA-213): key-id rotation for the start permit
All checks were successful
CI / Verify (pull_request) Successful in 41s
925af4533c
Add a `kid` to the start-permit claims (a wire-format version bump v1->v2) and a
distinct `kid` to the prepared-sources manifest, so a verifier can trust a keyset
of {kid, publicKey} and rotate Ed25519 keys with zero downtime.

- `dispatchStartPermitClaimsSchema` is now version 2 and carries `kid`; the v1
  vector is kept as a rejection fixture (`dispatch-start-permit-v1.json`) and the
  portable v2 vector is added (`dispatch-start-permit-v2.json`).
- `validateStartPermitClaims` takes a keyset, selects the public key by the
  claim's `kid` (unknown/missing kid is a bounded refusal), and still enforces the
  m10 clock-skew tolerance and every identity binding; it returns the selected key
  for the consumer's own Ed25519 verify (kid selects the key, never the algorithm).
- `selectDispatchVerificationKey` is the shared, total, crypto-free selector.
- The manifest carries its own `kid` inside the signed bytes next to the purpose
  domain tag; `sourceBindingSchema` and the runtime-observation binding omit `kid`
  because it is a key selector, not part of the attempt identity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
janpeter changed title from WIP: feat(IDEA-213): contracten voor managed queue dispatch (ST-1590) to feat(IDEA-213): contracten voor managed queue dispatch (ST-1590) 2026-09-21 18:00:09 +02:00
s4m-codex-reviewer requested changes 2026-09-21 18:03:42 +02:00
Dismissed
s4m-codex-reviewer left a comment

Verdict: REQUEST_CHANGES

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • ERROR — lib/queue-dispatch-validation.ts:41: dispatchStateSchema accepteert UNCERTAIN_UNSTARTED niet, terwijl DispatchState deze state wel exposeert en decideDispatchTransition('CLAIMED', 'lease_lost') hem produceert. Daardoor kunnen consumers een geldige dispatch-view/state uit de gedeelde state machine niet valideren, en juist de m11-safety split voor nooit-gestarte attempts wordt aan de API/contractrand onbruikbaar. Voeg UNCERTAIN_UNSTARTED toe aan dispatchStateSchema en dek dit expliciet met een validatietest.

Verificatie

  • node node_modules/typescript/bin/tsc --noEmit geslaagd op een verse clone met de PR-diff toegepast.
  • npm run verify/Vitest kon lokaal niet volledig draaien door sandbox/runtime-problemen met uitvoerrechten/native Rollup optional dependency (tsc: Permission denied, daarna failed to map segment from shared object bij Vitest).
# Verdict: REQUEST_CHANGES Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - **ERROR** — `lib/queue-dispatch-validation.ts:41`: `dispatchStateSchema` accepteert `UNCERTAIN_UNSTARTED` niet, terwijl `DispatchState` deze state wel exposeert en `decideDispatchTransition('CLAIMED', 'lease_lost')` hem produceert. Daardoor kunnen consumers een geldige dispatch-view/state uit de gedeelde state machine niet valideren, en juist de m11-safety split voor nooit-gestarte attempts wordt aan de API/contractrand onbruikbaar. Voeg `UNCERTAIN_UNSTARTED` toe aan `dispatchStateSchema` en dek dit expliciet met een validatietest. ## Verificatie - `node node_modules/typescript/bin/tsc --noEmit` geslaagd op een verse clone met de PR-diff toegepast. - `npm run verify`/Vitest kon lokaal niet volledig draaien door sandbox/runtime-problemen met uitvoerrechten/native Rollup optional dependency (`tsc: Permission denied`, daarna `failed to map segment from shared object` bij Vitest).
fix(IDEA-213): accept UNCERTAIN_UNSTARTED in dispatchStateSchema
All checks were successful
CI / Verify (pull_request) Successful in 42s
c65f5a845e
dispatchStateSchema omitted UNCERTAIN_UNSTARTED, so the shared validation
rejected a state the shared state-machine itself produces
(CLAIMED:lease_lost) and the projection handles (dispatchRootStatus →
claimed). The m11 safety split for never-started attempts was unusable at
the contract edge. Add the value after UNCERTAIN and cover it with a test
that also asserts decideDispatchTransition('CLAIMED','lease_lost') output
passes the schema. candidateStateSchema/attemptStateSchema left untouched
(their types do not carry this state).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
s4m-codex-reviewer left a comment

APPROVED

Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden.

Findings

  • Geen blokkerende findings gevonden.

Review-notities

  • De nieuwe lib/queue-dispatch* contractmodules blijven binnen de productstandaard voor scrum4me-shared: pure TypeScript, interne imports en zod, zonder runtime/server-afhankelijkheden in productcode.
  • De managed QUEUE_TASK/QUEUE_REVIEW defaults falen gesloten op onbeperkte tools, bypassPermissions en te brede Codex-sandboxes; dit past bij de job-config ownership-regels in de architectuurdoc.
  • De Prisma-uitbreiding houdt scrum4me-shared als canonieke schema-bron en markeert de SQL-only guards duidelijk voor de designated migrator.
  • Tests/fixtures dekken de belangrijkste contractranden: canonicalisatie, start-permit v2/kid, state transitions, projection mapping, document pinning en managed job defaults.

Niet lokaal uitgevoerd: er was geen PR-checkout/package in /opt/agent; review is uitgevoerd op de aangeleverde unified diff plus productdocs.

# APPROVED Geen gekoppeld plan gevonden — beoordeeld op codekwaliteit + product-standaarden. ## Findings - Geen blokkerende findings gevonden. ## Review-notities - De nieuwe `lib/queue-dispatch*` contractmodules blijven binnen de productstandaard voor `scrum4me-shared`: pure TypeScript, interne imports en `zod`, zonder runtime/server-afhankelijkheden in productcode. - De managed `QUEUE_TASK`/`QUEUE_REVIEW` defaults falen gesloten op onbeperkte tools, `bypassPermissions` en te brede Codex-sandboxes; dit past bij de job-config ownership-regels in de architectuurdoc. - De Prisma-uitbreiding houdt `scrum4me-shared` als canonieke schema-bron en markeert de SQL-only guards duidelijk voor de designated migrator. - Tests/fixtures dekken de belangrijkste contractranden: canonicalisatie, start-permit v2/kid, state transitions, projection mapping, document pinning en managed job defaults. Niet lokaal uitgevoerd: er was geen PR-checkout/package in `/opt/agent`; review is uitgevoerd op de aangeleverde unified diff plus productdocs.
Author
Owner

Review afgehandeld — F-S1 gerepareerd.

  • F-S1 (ERROR) lib/queue-dispatch-validation.ts: dispatchStateSchema accepteerde UNCERTAIN_UNSTARTED niet, terwijl de gedeelde state-machine (decideDispatchTransition('CLAIMED','lease_lost')) die state produceert. Toegevoegd aan het enum + validatietest (rood-eerst bevestigd). Commit c65f5a8.

Verificatie: npm test 538 passed. De vier consumers zijn apart herpind naar c65f5a8.

**Review afgehandeld — F-S1 gerepareerd.** - **F-S1 (ERROR)** `lib/queue-dispatch-validation.ts`: `dispatchStateSchema` accepteerde `UNCERTAIN_UNSTARTED` niet, terwijl de gedeelde state-machine (`decideDispatchTransition('CLAIMED','lease_lost')`) die state produceert. Toegevoegd aan het enum + validatietest (rood-eerst bevestigd). Commit `c65f5a8`. Verificatie: `npm test` 538 passed. De vier consumers zijn apart herpind naar `c65f5a8`.
Sign in to join this conversation.
No reviewers
No labels
severity/s4
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/scrum4me-shared!61
No description provided.