Repository navigation
Commit 76165fe
authored
RedMesh tenant drafts, generated packs and simplified activation (RM-109, RM-110, RM-112) (#529)
* feat(redmesh): tenant draft record and operations (RM-109 phase 2)
What changed:
- New `tenant_draft` tenancy record kind (`tenancy/drafts.py`): checklist and
coverage vocabulary, `covers` rules, applicability, item states and
transitions, effective dates, the stored-record validator, the completeness
rule and the draft DTO. Added to the adapter `_KINDS` with a `_validate`
branch and a `list_tenant_drafts` enumeration.
- Seven POST endpoints, full-portfolio Super-Tenant Admin only:
create/update/get/list/delete_tenant_draft and
upload/download_tenant_draft_document. The service decides under the
administration lock; document reads, writes and deletes run in the plugin
outside it. A set activation marker locks update, upload and delete
(409 draft_locked).
- Draft uploads reuse the contract checks (PDF, 50 MB) and write the
`redmesh_tenant_contract` envelope at schema_version 1.1 with `draft_id` and
`document_kind`; refusals are contract_invalid / document_invalid by slot.
The stored ref keeps the `_valid_contract` key set. A replaced file, or one
dropped by setting an item `missing`, is deleted after the row is written.
- A draft file gone from the store reads as `missing` and the row is
corrected on the next write; delete skips it. Delete removes every file,
then the record, and emits the `tenant_draft_deleted` audit event.
- Data maintenance: `tenant_draft` in `_TENANCY_KINDS`, current when the store
validator passes and old/unrecognized otherwise, decided before the
related-tenant check; its files are in the inventory via
`items.<kind>.document.ref`.
Why:
- RM-109 phase 2 of the onboarding-drafts contract: a super-tenant prepares a
tenant before its documents are signed. Activation (prepare_tenant with
draft_id, marker, release, close) is phase 3; resolve_contract and
prepare_tenant are unchanged.
Checks:
- docker exec rm4 python3 -m unittest test_tenant_drafts: 33 tests OK.
- test_tenant_contract, test_data_maintenance, test_tenant_administration,
test_tenant_delete, test_tenancy_administration_store, test_authz_surface:
176 tests OK (baseline 157 for the first five, +2 data maintenance).
- Full discover of red_mesh/tests: 3470 run; 30 errors are modules importing
pytest (not installed in the container), 1 timing failure in
test_postponed_analyze_native_ipc that passes when run alone.
* fix(redmesh): tenant draft answers never read or correct files (RM-109 phase 2)
What changed:
- Removed the document-store check from every draft answer: get, create,
update and upload build the DTO from the stored row only, and no write
changes an item because a file could not be read (the gone_refs path and
drafts.without_documents are gone).
- A file that cannot be read is detected only where it is used: download
answers 404 not_found from a single read (read_contract takes an `absent`
answer), delete skips it.
- A failed delete of an unreferenced file (replaced slot, item set missing,
rollback of a refused attach) is logged at error level with draft id, slot
and ref; the write stands.
- The draft validator also requires a stored document's mime to be
application/pdf.
- AGENTS.md entry dated to the commit date and reworded for the rule.
Why:
- R1fsDocumentStore.get answers a timeout as no file, so the previous check
persisted a transient failure as `missing`, and it read every file in full
on each answer. Review of 71b5c17; contract §Missing files is revised to
this rule.
Checks:
- docker exec rm4 python3 -m unittest test_tenant_drafts: 36 tests OK
(new: unreadable file never changes the row, answers need no store, gone
file 404/skip, delete-failure log, non-PDF stored ref fails closed).
- test_tenant_contract, test_data_maintenance, test_tenant_administration,
test_tenant_delete, test_tenancy_administration_store, test_authz_surface:
176 tests OK.
- Full discover of red_mesh/tests: 30 errors, all modules importing pytest
(not installed in the container); no other failure.
* feat(redmesh): tenant draft activation, release and close (RM-109 phase 3)
What changed:
- prepare_tenant takes an optional draft_id. The plugin reads the draft and
re-resolves its files outside the lock, each envelope bound to the draft and
its slot (resolve_draft_document); caller creation arguments are ignored.
Locked step: draft_changed, marker (activation_in_progress with holder),
draft_incomplete with missing[], existing checks, then on first
preparation the uploader rule and contract_in_use; the activation marker is
written before the receipt (clearing last_release).
- Both paths: the uploader of every document must hold the full-portfolio
Super-Tenant Admin role through the identity port (no longer the creator);
a replay is still bound by the receipt's sha256. contract_in_use scans the
raw tenant and receipt rows (503 past the namespace cap). The one-step path
refuses a draft envelope (contract_invalid).
- Receipt and tenant gain compliance_types, framework_agreement,
data_handling and governance {coverage, applicability, effective}, bound
like contract (receipt also draft_id); one-step tenants are unchanged.
get_tenant_contract returns them, new download_tenant_document serves any
of the three, and tenant delete removes all three files.
- release_tenant_draft_activation (marker's receipt; tenant_active; deletes
the inactive tenant row, the bound domain reservation and the receipt, no
file; last_release written first so every crash point resumes; retry
answers last_release; not_activating) and close_tenant_draft
(activation_not_complete; deletes the record only). Draft answers carry
activation.tenant_state. Audit event tenant_draft_activation_released.
- Tests: contract_terms() mints a fresh reference per call and
install_contract takes one, as R1FS gives each upload its own CID.
Why:
- RM-109 phase 3 of the onboarding-drafts contract: activation of a tenant
draft into the existing tenant creation, with recovery of a stuck
activation.
Checks:
- rm4 unittest: test_tenant_drafts 54 OK; test_tenant_contract,
test_data_maintenance, test_tenant_administration, test_tenant_delete,
test_tenancy_administration_store, test_authz_surface, test_tenant_drafts:
239 OK.
- rm4 full discover of red_mesh/tests: 3496 run, 30 errors, all modules
importing pytest (absent in the container); those 30 modules plus the two
tests importing them pass under the repo .venv pytest (2775 + 2).
* fix(redmesh): draft release edge cases and refusal shape (RM-109 phase 3)
What changed:
- A tenant marked `deleting` refuses a draft release with the existing
409 tenant_deleting instead of tenant_active.
- A release resumed after its receipt was already removed (last_release
names the marker's request) clears the marker as a replay, so it emits no
second audit event.
- A refusal's details are merged before the fixed keys, so they can never
override success/status/status_code/error.
- `_draft_answer` documents that one corrupt receipt fails the drafts list
closed (503) on purpose; the drafts module docstring describes phase 3.
- Tests: the release-after-tenant-delete case drives the real
delete_tenant, plus a delete that stops between the receipt and the
tenant-row delete; tenant_deleting; replayed release without an event;
refusal shape; raw_rows failing closed on an undecodable row.
Why:
- Review of 48063b9. Note: that commit's body says the targeted run was
239 tests; it was 235.
Checks:
- rm4 unittest: test_tenant_drafts 58 OK; with test_tenant_contract,
test_data_maintenance, test_tenant_administration, test_tenant_delete,
test_tenancy_administration_store and test_authz_surface: 239 OK.
- rm4 full discover of red_mesh/tests: 3500 run, 30 errors (modules
importing pytest, absent in the container), 1 timing failure in
test_postponed_analyze_native_ipc that passes when run alone.
* fix(redmesh): audit a draft release once, when its marker is cleared
What changed:
- release_tenant_draft_activation reports whether this call cleared the
marker (`cleared`) instead of `replayed`; the plugin emits
tenant_draft_activation_released on that call only. A release resumed
after its receipt delete emits the event when it clears the marker, with
rows_deleted counting only that call's deletes; a call that finds no
marker and answers last_release emits nothing. The answer is unchanged.
Why:
- Every release passes the marker clear exactly once, so the event is
emitted once per release, including one that crashed part-way (18ae0db
left such a release unaudited).
Checks:
- rm4 unittest: test_tenant_drafts 58 OK (crash after the receipt delete:
the finishing call emits exactly one event, a further call none); with
test_tenant_contract, test_data_maintenance, test_tenant_administration,
test_tenant_delete, test_tenancy_administration_store and
test_authz_surface: 239 OK.
* feat(redmesh): engagement drafts inside the tenant draft (RM-109 phase 4)
What changed:
- New `engagement_draft` record kind (`tenancy/engagement_drafts.py`, `ted_<uuid>`,
`parent` = `{draft_id}` or `{tenant_id}`), added to the adapter `_KINDS`/`_validate` and to
data maintenance with the parent rule (`current` with a live parent, `orphan/parent_gone`
otherwise, never `old_tenant`; files via `document.document.ref` and `document.generated.ref`).
- Eight STA-only endpoints: create/update/get/list/delete engagement draft, pack upload and
download (`redmesh_tenant_contract` 1.1 envelope with `document_kind: engagement_pack` and
`engagement_draft_id`), and `activate_engagement_draft` as one locked call: `parent_not_active`,
replay once `en_<uuid>` exists (whoever calls), completeness with `missing[]`/`reasons`, the
existing engagement body with the signed pack as `agreement` and the generated pack as a second
`other` document, pack uploaders checked for the platform role, `contract_in_use` over
engagement `documents[]`, row deleted last.
- Every engagement draft write is `draft_locked` while the parent tenant draft's marker is set.
- `read_engagement_document` accepts the 1.1 pack envelope.
- `close_tenant_draft` re-homes the children before deleting the row; `delete_tenant_draft`
cascades (children's files, rows, then its own) and answers `engagement_drafts_deleted`.
- Collapsed tenant checklist: `contract` covers both tenant records, applicability `required`
and no longer decides completeness, `items_total` counts the shown item, `generated` blocks
validated on items and the pack slot and kept across `missing`/`awaiting_signature`.
- `ted_` ids answer 404 as tenant and as engagement ids.
Why:
- Contract `onboarding-drafts.md` §Engagement draft and §Checklist (owner, 2026-10-07): the whole
pack is prepared inside the tenant draft before the client signs anything; each engagement
activates on its own signed pack.
Checks:
- rm4: focused suites (engagement drafts, tenant drafts, engagements, data maintenance, authz
surface, tenant contract/administration/delete) 281 tests OK.
- host .venv pytest, full red_mesh tests: 6291 passed, 5 skipped, 3448 subtests.
* fix(redmesh): engagement draft review fixes (RM-109 phase 4)
What changed:
- `validate_tenant_draft` completes an item written before the `generated` slot existed with
`generated: None`, on a copy the adapter returns, so phase 2-3 rows read as current instead of
`old/unrecognized`, and a read never changes the stored bytes (the maintenance hash still matches).
- `attach_tenant_draft_document` refuses `409 same_as_generated` when the upload's sha256 equals the
slot's `generated.sha256`, as the engagement pack upload does.
- `items.<kind>.covers` and `applicability.<record>` are no longer change keys of
`update_tenant_draft` (`400 invalid_request`): the pack fixes them.
- `activate_engagement_draft` answers `tenantId` from the locked step; the plugin audits
`engagement_created` under it instead of the pre-lock parent.
- `delete_tenant_draft` cascade: a `404 not_found` from a child's row delete (removed concurrently)
counts as done.
- Module memory notes that one malformed `engagement_draft` row fails close, cascade delete and list
closed until data maintenance removes it.
Why:
- Review of eaa98ff (APPROVE_WITH_NITS): keep pre-phase-4 drafts readable, mirror the
`same_as_generated` rule on the tenant pack, honour "never edited" for covers and applicability,
audit with the tenant the engagement was created under, and tolerate a concurrent child delete.
Checks:
- rm4: focused suites (engagement drafts, tenant drafts, engagements, data maintenance, authz
surface, tenant contract/administration/delete) 283 tests OK.
* feat(redmesh): super-tenant profile, party block and generated-pack baseline (RM-110 phase 1)
What changed:
- `super_tenant_profile` record kind (`tenancy/super_tenant_profile.py`, one row per deployment,
`ids = ["deployment"]`) with `get_super_tenant_profile` / `update_super_tenant_profile`
(STA-only, partial update, audited as `super_tenant_profile_updated` with the changed keys only);
adapter `_KINDS` / `_validate` and the data-maintenance rule (always current, never orphan).
- Tenant draft and tenant `legal` gain the optional party fields `address`, `vat_id`,
`contact_name`, `contact_email`, `contact_phone`: `_legal` accepts a four-key block unchanged and
writes no default, the draft validator completes an old block on a copy, DTOs answer absent
optional fields as "", `prepare_tenant(draft_id)` copies and binds the whole block.
- `store_generated_document` for a tenant draft (`contract`) or an engagement draft
(`engagement_pack`): the backend hashes the bytes and the snapshot, stores both in one
`redmesh_tenant_contract` 1.1 envelope (`role: generated`), writes the `generated` block and the
new item state `generated` (from missing / generated / awaiting_signature; `409 already_signed`
otherwise); the previous generated file is deleted after the new ref is written; refusals
before any store write.
- `contract_generated` on receipt and tenant: copied at activation when the draft has a baseline,
optional and outside the receipt's all-or-none set, bound when present (extra-on-tenant guard),
returned by `get_tenant_contract`, deleted with the tenant.
Why:
RM-110 phase 1 of the onboarding-drafts contract: the provider party block and the customer party
fields the generated packs are rendered from, and the backend side of the generation baseline, so
the Navigator renderer (phase 2) has a place to store its packs and snapshots.
Checks:
- rm4: unittest over test_super_tenant_profile, test_tenant_drafts, test_engagement_drafts,
test_tenant_contract, test_tenant_administration, test_tenant_delete,
test_tenancy_administration_store, test_data_maintenance, test_authz_surface,
test_tenant_engagements: 313 tests OK.
- host .venv pytest extensions/business/cybersec/red_mesh/tests: 6312 passed, 5 skipped,
3522 subtests passed.
* fix(redmesh): generated-pack review fixes (RM-110 phase 1)
What changed:
- `release_tenant_draft_activation`, tenant-deleted branch: the generated baseline is dropped with
the signed copy (`generated: null`, state `missing` when it was `generated`), since the tenant
delete removed `contract_generated.ref`, the same CID the draft names.
- `prepare_tenant(draft_id)`: the plugin re-reads and re-hashes `items.contract.generated.ref` as
well (`resolve_draft_document`, slot `contract`; a gone or tampered file is `contract_invalid`),
and the locked step includes it in the `draft_changed` comparison, as `activate_engagement_draft`
does for its pack.
- `drafts.normalize_snapshot`: a cheap character-length pre-check before the UTF-8 length.
- `drafts.py` exports `LEGAL_MAX`, `nonempty_text`, `text` (private aliases kept);
`super_tenant_profile.py` imports those.
Why:
Review of 3920034 (APPROVE_WITH_NITS): a released draft must not keep a block whose file is gone,
and the tenant path should verify the generated pack the way the engagement path does.
Checks:
- rm4: unittest over test_super_tenant_profile, test_tenant_drafts, test_engagement_drafts,
test_tenant_contract, test_tenant_administration, test_tenant_delete,
test_tenancy_administration_store, test_data_maintenance, test_authz_surface,
test_tenant_engagements: 315 tests OK.
* test(redmesh): engagement draft ids never resolve on node assignment
What changed: TestEngagementDraftIdsNeverResolve also probes set_tenant_node_assignment with a ted_ id.
Why: pins the 404 not_found answer already confirmed by a read-only probe.
Checks: test_engagement_drafts, 31 tests OK.
* feat(redmesh): tenant activation without an admin, draft node plan and private hold (RM-112 phase 2)
What changed:
- Admin optional on a draft activation: None is canonical on the intent, the receipt and the
tenant (draft "" normalised in _draft_terms); the receipt validator takes initial_admin_id and
initial_admin_generation both or neither; prepare_tenant and activate_tenant read or check no
account when there is none; root_admin_id stays None. Completeness no longer lists
field:initial_admin_id. The one-step prepare_tenant still requires an admin.
- Draft `nodes` plan ({node_address, mode}, validated with valid_node_address and
NODE_ASSIGNMENT_MODES, sorted, one per node), written through update_tenant_draft, in the DTO and
the list row; a row stored before it reads back with nodes [].
- Private hold: _assign_node refuses a node held by a draft's private plan with 409
node_planned_private, except for the tenant whose receipt names that draft; update_tenant_draft
refuses planning a node another draft holds privately, and a private plan while the node has a
live assignment (node_private_assigned / node_shared_assigned). The holding draft's id and name
are returned only to a full-portfolio Super-Tenant Admin. Delete and close drop the plan with
the row.
- begin_tenant_draft_delete refuses 409 draft_has_signed_documents while any draft item or child
engagement pack is signed.
- Tenant row (list and detail) gains nodeCount, the live assignments.
Why: RM-112 owner decisions 3, 4, 14, 15, 17 and 20: a draft tenant activates without a user,
plans its nodes on the record, and a draft with signed documents is kept.
Checks: host .venv pytest, red_mesh tests (excluding e2e): 6334 passed, 5 skipped. rm4
unittest on the draft, node, administration and data-maintenance modules: 251 OK. Existing
delete tests now use unsigned generated packs, since a signed draft is no longer deletable.
* feat(redmesh): refuse deleting a signed engagement draft
What changed:
- begin_engagement_draft_delete refuses 409 draft_has_signed_documents when the
draft's pack is signed, before any file is deleted; the finish path's race
check is unchanged.
- Engagement draft delete tests now delete unsigned drafts; new tests cover the
signed refusal (and that unsigning first allows it) and a draft with no file.
Why:
- Owner decision 2026-10-08 (RM-112): a signed engagement pack keeps its draft,
as the tenant draft rule does (decision 20).
Checks:
- pytest test_engagement_drafts/test_tenant_drafts/test_tenant_draft_setup: 121 passed
- pytest red_mesh tests excluding e2e: 6322 passed, 5 skipped
* feat(tenancy): a tenant draft activates only with its contract effective date
What changed:
- drafts.completeness lists field:contract.effective_from while the contract item has no effective
date; an end date stays optional (none = an indefinite term).
- Test helpers set the date on ready drafts; a test covers the refusal and the indefinite term.
Why:
- RM-112 decision 24: the contract term is set before the tenant pack is generated and printed in
it, so a tenant is never activated on a contract without a start date.
Checks:
- docker exec rm4 python3 -m unittest (tenant contract, administration, delete, data maintenance,
store, documents, drafts, engagement drafts, profile, authz, engagements, draft setup): 347 OK
- full red_mesh unittest discover: 3576 run; the 30 errors are the existing pytest-import failures
in the container
* chore: inc ver1 parent f48ae10 commit 76165fe
23 files changed
Lines changed: 5051 additions & 82 deletions
File tree
- extensions/business/cybersec/red_mesh
- services
- tenancy
- adapters
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
579 | 579 | | |
580 | 580 | | |
581 | 581 | | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
| 591 | + | |
| 592 | + | |
| 593 | + | |
| 594 | + | |
| 595 | + | |
| 596 | + | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
| 616 | + | |
| 617 | + | |
| 618 | + | |
| 619 | + | |
| 620 | + | |
| 621 | + | |
| 622 | + | |
| 623 | + | |
| 624 | + | |
| 625 | + | |
| 626 | + | |
| 627 | + | |
| 628 | + | |
| 629 | + | |
| 630 | + | |
| 631 | + | |
| 632 | + | |
| 633 | + | |
| 634 | + | |
| 635 | + | |
| 636 | + | |
| 637 | + | |
| 638 | + | |
| 639 | + | |
| 640 | + | |
| 641 | + | |
| 642 | + | |
| 643 | + | |
| 644 | + | |
| 645 | + | |
| 646 | + | |
| 647 | + | |
| 648 | + | |
| 649 | + | |
| 650 | + | |
| 651 | + | |
| 652 | + | |
| 653 | + | |
| 654 | + | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
| 658 | + | |
| 659 | + | |
| 660 | + | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
0 commit comments