FLOW-AUTHORING-WRITEBACK-CONTRACT-7A-L1.md markdown
317 lines 20.6 KB
Raw
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 16 hours ago

Flow Authoring Write-Back — Canonical Contract (Phase 7A, Step 7A-L1a)

Status: Contract only — Thinking step (7A-L1a). This is the frozen, canonical contract for live Flow authoring write-back: how a drafted/edited/imported Flow becomes a reviewed, durable change in Knowtation. No implementation, no routes, no live effect, and no posture flip ship in this step. The mechanical implementation (propose facade + seven-tier test bodies) is 7A-L1b (Auto), written to this contract without redesigning it.

Authored on branch feat/flow-projection-pilot (Knowtation). Always target the repo explicitly with muse -C ~/knowtation ….

Related:

  • docs/FLOW-V0-SPEC.md — the canonical Flow spec. §3 names the flow_propose MCP tool / POST /api/v1/flows propose route; §6 item 5 ("durable edits route through proposals") is the security gate this contract satisfies.
  • docs/FLOW-STORE-CONTRACT-7A-10.md — the read store this write-back lands into; §1.4 seeding is the only existing write, and §8 rule 6 states "all durable user changes route through proposals." This contract is that path.
  • docs/openapi.yaml/proposals (lifecycle) — the existing proposal create → evaluate → approve → apply machinery this contract reuses. No new write path.
  • lib/note-state-id.mjs — the kn1_ optimistic-concurrency token used as the proposal base_state_id; the Flow analog is defined in §4 below.
  • scooling/docs/FLOW-AUTHORING-LIVE-WIRE-CONTRACT-7A-L1.md — the consumer side (FlowAuthoringAdapter live wire) ratified field-for-field against this contract.

Scope fence (7A-L1a): propose-shape + scope/role write-authority + review-before- write lifecycle + optimistic concurrency + the seven-tier test matrix only. Not in scope: any lib/flow/ write implementation, the Hub route, CLI/MCP wiring, OpenAPI edits (those land with the route in 7A-L1b per the no-docs-only-PR rule), candidate promotion (7A-L4), run advancement (7A-L3), or MuseHub enrichment (7A-L5).


Simple summary

Today a Flow can only be read (list/get/project). This contract describes the one safe way to change a Flow: you never edit the canonical copy directly. Instead you hand in a proposal — "here is the Flow I'd like to add or change, and why." A human (or a policy) reviews it, and only after it is approved does the canonical Flow change. Three things are guaranteed. First, nothing is ever written without review — there is no back door. Second, you can only propose changes to Flows in a scope you are allowed to write (your own personal Flows always; project/org Flows only if your role grants it) and the server decides this, never the client. Third, if the Flow changed since you started editing, your proposal is rejected as a conflict rather than silently overwriting someone else's work. This reuses the exact proposal machinery notes already use, so it inherits a proven, audited, safe path.

Technical summary

Flow authoring write-back is a typed facade over the existing proposal lifecycle (/proposals create → evaluation → approve → apply), not a second write path. A flow_propose MCP tool and POST /api/v1/flows route accept a validated knowtation.flow/v0 + flow_step/v0 bundle (reusing validateFlowBundle), derive a proposal targeting the Flow's vault_mirror_path note, and return a knowtation.flow_proposal/v0 envelope ({ proposal_id, flow_id, base_version, base_state_id, status: "proposed" }). Write authority is scope × role, resolved server-side and deny-by-default (personal for any authenticated writer; project requires editor; org requires admin), reusing resolveFlowVisibleScopes plus a new write-tier check. Optimistic concurrency uses base_version (the semver the edit is based on) and base_state_id (a deterministic flowst1_ hash of the canonical flow+steps); a mismatch at approve time is 409 FLOW_LINEAGE_CONFLICT. auto_approvable is server-derived from verification kinds (human_review ⇒ false) so a draft can never self-authorize. All free-text (instruction, boundaries, summary, intent) is untrusted; evidence/lineage/provenance are pointers/hashes only; no secret is ever serialized. FLOW_AUTHORING_WRITES stays disabled by default and automatable step execution remains inert (7A-L3).


0. Design decision (recorded as SD-4)

How does Flow authoring map onto the canonical write path? Recorded once in scooling/docs/CROSS-REPO-COORDINATION.md → Standing Decisions as SD-4:

SD-4 — Flow authoring write-back is a typed facade over the existing /proposals lifecycle, not a new write path. flow_propose / POST /api/v1/flows validate a Flow bundle, then create a standard proposal targeting the Flow's vault_mirror_path note (frontmatter type: flow). Review/evaluation/approve/apply and the base_state_id concurrency check are the same machinery notes use. Rationale: one audited review path, no duplicated approve/apply logic, inherits the proven optimistic- concurrency + evaluation gate. The Flow index is updated only on approve, by reconciling the approved mirror note back into the index (the mirror is a projection of the index everywhere else; on the write path the approved mirror is the reconciliation source).

This keeps the boundary "no new write path; no adapter writes canonical knowledge directly" (FLOW-V0-SPEC §6 item 5) literally true.


1. Surfaces (triple-exposed, identical contract)

Same shape across MCP / Hub REST / CLI, mirroring the read parity of 7A-10. All three require FLOW_AUTHORING_WRITES enabled (default off, §6) and resolve write authority server-side.

Surface Propose new Propose edit Import bundle
MCP flow_propose (draft) flow_propose (flow_id + base_version) flow_import (bundle)
Hub REST POST /api/v1/flows POST /api/v1/flows/{id}/proposals POST /api/v1/flows/import
CLI knowtation flow propose <bundle.json> same (bundle carries flow_id+base_version) knowtation flow import <bundle.json>

All three converge on one handler (handleFlowProposeRequest) that validates, checks write authority, and delegates to the proposal create lifecycle. No surface re-implements the write — parity is proven by deep-equality (§7 tier 2).

1.1 Request — flow_propose (new or edit)

{
  "flow": { /* knowtation.flow/v0 — full record (FLOW-V0-SPEC §1.3) */ },
  "steps": [ /* knowtation.flow_step/v0[] — full anatomy (§1.4) */ ],
  "intent": "string",            // REQUIRED, untrusted; recorded on the proposal, never executed
  "base_version": "1.2.0",       // REQUIRED for edit (the version the edit is based on); OMITTED for new
  "base_state_id": "flowst1_…"   // REQUIRED for edit (concurrency token, §4); OMITTED for new
}
  • New flow: base_version + base_state_id omitted; server treats it as "must still be absent" (the absent-flow sentinel, §4). A flow_id collision with an existing flow in scope ⇒ 409 FLOW_LINEAGE_CONFLICT.
  • Edit: flow_id (inside flow), base_version, and base_state_id all required.
  • The bundle is validated with the existing validateFlowBundle (FLOW-V0-SPEC §1 anatomy completeness; a step missing trigger/when_not_to_run/output_shape/ verification400 FLOW_DRAFT_INVALID).

1.2 Response — knowtation.flow_proposal/v0

{
  "schema": "knowtation.flow_proposal/v0",
  "proposal_id": "string",       // the underlying /proposals id (lineage pointer)
  "flow_id": "flow_weekly_review",
  "base_version": "1.2.0",       // echo (null for new)
  "base_state_id": "flowst1_…",  // the state the proposal was based on (null for new)
  "scope": "personal|project|org",
  "auto_approvable": false,      // SERVER-DERIVED (§3); human_review ⇒ false
  "status": "proposed",
  "review_queue": "string"       // where it routes; never a secret
}

The response carries pointers + labels only — no rendered Flow body, no secrets.


2. Write-authority model (scope × role, server-side, deny-by-default)

Read visibility (7A-10 §4) is necessary but not sufficient to write. Write authority is a second, stricter gate.

Target scope Minimum role Resolution
personal any authenticated writer (viewer+) writing their own vault resolveFlowVisibleScopes must include personal
project editor visibleScopesForRole must include project and role ≥ editor
org admin role must be admin
Rule Contract
Authorization, not a filter Write tier resolved server-side from verified identity + role + WorkspaceScopeAdapter. The client never supplies its own write tier.
Deny by default Absent/ambiguous resolution ⇒ personal only. A personal writer proposing a project/org Flow ⇒ 403 FLOW_SCOPE_DENIED. Ambiguous ⇒ 400 FLOW_SCOPE_AMBIGUOUS.
No scope widening from inside The draft's flow.scope is validated against the resolved write tier; a draft can never request a tier above what the actor holds (this is checked before the proposal is created).
No existence leak Proposing an edit to a flow_id the actor cannot read404 unknown_flow (identical to truly-missing), never 403.
Vault binding X-Vault-Id + role required on the Hub (requireVaultAccess + requireRole), parity with the read routes. CLI binds the locally-configured vault.

3. Review-before-write lifecycle (reuses /proposals)

The Flow proposal is a standard proposal record; it traverses the existing lifecycle. No new states are introduced.

flow_propose / POST /api/v1/flows
        │  validate bundle (validateFlowBundle) + write-authority + concurrency precheck
        ▼
   POST /proposals  ──────────────►  status: proposed     (intent, base_state_id, external_ref, source:"flow")
        │                                   │
        │                          (optional) /review-hints, /enrich   ← never a merge gate
        │                                   │
        ▼                                   ▼
   /proposals/{id}/evaluation  ────►  pass | fail | needs_changes   (EVALUATION_REQUIRED policy)
        │
        ▼
   /proposals/{id}/approve  ───────►  status: approved → APPLY
        │   base_state_id re-checked here (CONFLICT → 409)
        ▼
   reconcile approved mirror note → Flow index   (the ONLY index write besides seed)
Rule Contract
No silent promotion A drafted/imported Flow is never written to the index by flow_propose. The index changes only at approve→apply.
auto_approvable is server-derived Computed from the bundle's verification kinds: if any step is human_review (or evidence_required with a runtime-gated kind), auto_approvable = false. A draft has no auto_approvable field; supplying one is ignored.
Evaluation gate honored When HUB_PROPOSAL_EVALUATION_REQUIRED is set, approve requires a passed evaluation or an admin waiver_reason — unchanged from the note path.
Untrusted intent intent is recorded verbatim as data; it is never interpreted, never executes, and cannot widen scope.
Apply reconciliation On approve, the approved mirror note (frontmatter type: flow, body = the validated bundle) is reconciled into the Flow index by (flow_id, version) upsert; steps re-validated; a failed reconcile rolls back (no partial index write).

4. Optimistic concurrency — base_version + base_state_id

The Flow analog of the note kn1_ token. Prevents lost updates when two authors edit the same Flow.

Item Definition
flowStateId(flow, steps) 'flowst1_' + fnv1a64Hex(stableStringify({ flow, steps })) — a deterministic 64-bit FNV-1a over the canonical, key-sorted flow definition + ordered steps. Reuses fnv1a64Hex + stableStringify from lib/note-state-id.mjs.
Absent-flow sentinel 'flowst1_' + fnv1a64Hex(0x00) — used for new flows so a propose-new can require "still absent" and collide-fail if the flow_id appeared meanwhile.
base_version The semver the edit is based on; must equal the latest visible canonical version at propose time and at approve time.
Check points (1) propose-time precheck (fast fail if already stale); (2) approve-time authoritative re-check (the binding one).
Conflict Either base_version ≠ latest or base_state_idflowStateId(canonical)409 FLOW_LINEAGE_CONFLICT. The author re-fetches (flow get), rebases, and re-proposes.
Semver classification (advisory) Per FLOW-V0-SPEC §10 item 3: remove step / tighten verification = MAJOR; add optional step = MINOR; reword = PATCH. The contract records the proposed flow.version; the server validates it is strictly greater than base_version for an edit (else 400 FLOW_DRAFT_INVALID).

Store keying note (carry-forward from 7A-12): the 7A-10b store keys step bodies by step_id (= flow_id#ordinal) only, not (step_id, version). Until a versioned- step-keying slice (recommended 7A-10c) lands, an in-place step-field edit cannot diverge across two stored versions of one Flow. 7A-L1b MUST therefore reconcile an edit as a new version record (new (flow_id, version) row) and never mutate an existing version's step bodies in place. This is a hard constraint on the impl step.


5. Import (portable bundle → scope-checked proposal)

flow_import / POST /api/v1/flows/import accepts a portable bundle (the export shape from the read/derive side) and routes it through the same propose path.

Rule Contract
Scope-checked The bundle's scope is validated against the actor's write tier (§2). Unwritable ⇒ 403 FLOW_IMPORT_SCOPE_DENIED.
Malformed fails closed A bundle that fails validateFlowBundle400 FLOW_IMPORT_BUNDLE_MALFORMED; never partially imported.
Lineage preserved external_ref + source_vault_hint (pointers/labels only) are carried onto the proposal's external_ref; no field loss vs the export.
Tool allowlist external_tool skill-refs remain inert (FLOW-V0-SPEC §6 item 3); an imported Flow cannot run any external tool on import (that is the 7A-L2 external-agent gate).
Never auto-applied Import creates a proposed proposal — identical review path; never a direct index write.

6. Posture / gating (default off)

Control Default Effect
FLOW_AUTHORING_WRITES (Hub/CLI env + policy file, tri-state like hub-proposal-policy) off When off, flow_propose/flow_import return 403 FLOW_AUTHORING_DISABLED. No write path is reachable.
automatable step execution inert Drafting a step with automatable is parse-valid but execution stays gated (7A-L3); the proposal records it, nothing runs.
Classroom / minor policy may forbid An org policy may forbid authoring entirely or cap verification/automatable kinds (403 FLOW_AUTHORING_POLICY_FORBIDDEN).

Enabling FLOW_AUTHORING_WRITES is a Tier 3 action (named authorization), separate from this contract step and from the Scooling posture flag.

6.1 Error taxonomy (opaque codes; no scope/id/secret leak)

FLOW_DRAFT_INVALID · FLOW_SCOPE_DENIED · FLOW_SCOPE_AMBIGUOUS · unknown_flow (missing or unreadable) · FLOW_LINEAGE_CONFLICT (409) · FLOW_IMPORT_SCOPE_DENIED · FLOW_IMPORT_BUNDLE_MALFORMED · FLOW_AUTHORING_DISABLED · FLOW_AUTHORING_POLICY_FORBIDDEN · EVALUATION_REQUIRED (from approve, unchanged).


7. Seven-tier test matrix (what each tier proves — design only)

Per RULE #0. 7A-L1b ships all seven tiers under test/flow-authoring-*.test.mjs, reusing the six flows/starter/ bundles + a malicious-step bundle + a higher-scope bundle + an empty vault. No network in unit tests. Every tier runs with FLOW_AUTHORING_WRITES toggled both ways.

Tier File What it proves (representative cases)
unit test/flow-authoring-unit.test.mjs flowStateId is deterministic + key-order-stable; absent-sentinel is stable; auto_approvable derives false for any human_review step; validateFlowBundle rejects anatomy-incomplete drafts; the proposal envelope stamps knowtation.flow_proposal/v0.
integration test/flow-authoring-parity-integration.test.mjs MCP flow_propose, POST /api/v1/flows, and CLI flow propose produce a deep-equal proposal envelope for the same authorized request; all three create exactly one /proposals record; FLOW_AUTHORING_WRITES=off ⇒ all three return FLOW_AUTHORING_DISABLED.
e2e test/flow-authoring-e2e.test.mjs propose-new → evaluation pass → approve → flow get shows the new Flow at its version; propose-edit with correct base_version+base_state_id → approve → version bumped, old version still pinnable; discard leaves the index unchanged.
stress test/flow-authoring-stress.test.mjs MAX_STEPS_PER_FLOW-step drafts; many concurrent proposals against one flow_id — exactly one approves, the rest hit FLOW_LINEAGE_CONFLICT; no index corruption (atomic apply).
data-integrity test/flow-authoring-data-integrity.test.mjs approve→apply reconciliation preserves steps/skill-refs/verification/scope/version/lineage byte-for-byte; an edit creates a new version row (never mutates an existing version's steps — §4 carry-forward); rolled-back apply leaves zero partial state.
performance test/flow-authoring-performance.test.mjs propose validation + flowStateId within a p95 budget on the large fixture; approve→apply bounded; no quadratic scans in version resolution.
security test/flow-authoring-security.test.mjs scope denial (personal writer cannot propose project/org); ambiguous fails closed; no scope widening (draft scope above tier rejected pre-create); no existence leak (edit to unreadable flow ⇒ unknown_flow); injection (instruction/intent returned/recorded inert, never executed, cannot escalate); concurrency (stale base_state_id ⇒ conflict, no lost update); no secretsJSON.stringify of every proposal envelope + applied record carries no token/oauth/refresh_token/raw-content marker; provenance.actor is a hash.

8. Acceptance (7A-L1a)

  • Propose request/response shapes, the write-authority model, the review-before-write lifecycle (reusing /proposals), the base_version+base_state_id concurrency model, the import path, gating, the error taxonomy, and the seven-tier test matrix are all frozen here — contract only, no implementation, no route, no OpenAPI edit, no posture flip.
  • Ratified against FLOW-V0-SPEC.md (§3 surfaces, §6 security items 1/2/3/5/6), FLOW-STORE-CONTRACT-7A-10.md (read store + §8 rule 6), the existing /proposals lifecycle, and the consumer contract scooling/docs/FLOW-AUTHORING-LIVE-WIRE-CONTRACT-7A-L1.md.
  • SD-4 recorded in scooling/docs/CROSS-REPO-COORDINATION.md.
  • Muse-committed on feat/flow-projection-pilot; handover regenerated to point at 7A-L1b (Auto: propose facade + route + CLI/MCP + OpenAPI + seven-tier impl, with FLOW_AUTHORING_WRITES defaulting off).

Non-goals (7A-L1)

  • No candidate promotion/dismissal write-back (FlowCaptureAdapter writes — 7A-L4).
  • No run advancement / automatable execution (7A-L3).
  • No MuseHub enrichment — step-level history, semver-typed change impact, releases, provenance-anchored evidence (7A-L5).
  • No flip of FLOW_AUTHORING_WRITES (Knowtation) or FLOW_AUTHORING_WRITES_AUTHORIZED (Scooling) — both stay off; enabling is a separate Tier 3 authorization.

Handoff notes (for 7A-L1b — Auto)

  1. Branch is feat/flow-projection-pilot; this contract is Muse-committed. Always target Knowtation with muse -C ~/knowtation ….
  2. Implement lib/flow/flow-authoring.mjs (propose facade) + flowStateId in/near lib/note-state-id.mjs; wire POST /api/v1/flows, POST /api/v1/flows/{id}/proposals, POST /api/v1/flows/import, the flow_propose/flow_import MCP tools, and the knowtation flow propose|import CLI branch — all delegating to one handler.
  3. Add the OpenAPI shapes for the propose/import routes in the same change as the routes (no docs-only PR to main).
  4. Honor the §4 store-keying carry-forward: reconcile edits as new version rows.
  5. FLOW_AUTHORING_WRITES defaults off; ship all seven tiers green before any handover regen.
File History 1 commit
sha256:8915fe406161f95c1681f9469375e7bae5b28c884f00bedbdef65e4b0cd0738d docs(flow): commit FLOW-V0-SPEC.md hygiene for 7A-INT merge Human 16 hours ago