From 196e8532313dd8a5ffcea49f3a762de071406223 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:06:37 -0700 Subject: [PATCH 01/49] test: define governed offer approval contract --- .github/workflows/offer-approval-quality.yml | 57 ++++ packages/offer-approval/pyproject.toml | 24 ++ packages/offer-approval/tests/test_packet.py | 257 +++++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 .github/workflows/offer-approval-quality.yml create mode 100644 packages/offer-approval/pyproject.toml create mode 100644 packages/offer-approval/tests/test_packet.py diff --git a/.github/workflows/offer-approval-quality.yml b/.github/workflows/offer-approval-quality.yml new file mode 100644 index 000000000..1ecff719a --- /dev/null +++ b/.github/workflows/offer-approval-quality.yml @@ -0,0 +1,57 @@ +name: Offer Approval Quality + +on: + pull_request: + branches: + - develop + paths: + - "packages/offer-approval/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/offer-approval-quality.yml" + - "docs/adr/0017-governed-offer-approval.md" + - "docs/doctoring/offer-approval-references.md" + - "docs/traceability/offer-approval.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: offer-approval-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + unit: + name: Offer approval contract and 100% coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout exact candidate + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + - name: Prove exact candidate checkout + env: + ORGMETRA_EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: test "$(git rev-parse HEAD)" = "$ORGMETRA_EXPECTED_HEAD_SHA" + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14" + check-latest: false + - name: Install reviewed test toolchain + run: | + python -m pip install --require-hashes --no-deps --only-binary=:all: -r .github/requirements/foundation-test.txt + python -m pip check + - name: Compile offer approval package + run: python -m compileall -q packages/offer-approval/src packages/offer-approval/tests + - name: Test offer approval with exact statement and branch coverage + env: + PYTHONPATH: packages/offer-approval/src + COVERAGE_FILE: /tmp/orgmetra-offer-approval.coverage + run: python -m pytest -c packages/offer-approval/pyproject.toml packages/offer-approval/tests + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" diff --git a/packages/offer-approval/pyproject.toml b/packages/offer-approval/pyproject.toml new file mode 100644 index 000000000..5fdf2f8df --- /dev/null +++ b/packages/offer-approval/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools>=69"] +build-backend = "setuptools.build_meta" + +[project] +name = "orgmetra-offer-approval" +version = "0.1.0" +description = "Governed human offer-approval evidence for Orgmetra." +requires-python = ">=3.12" + +[project.optional-dependencies] +test = ["pytest>=8.3", "pytest-cov>=5.0"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = [ + "--cov=orgmetra_offer_approval", + "--cov-branch", + "--cov-report=term-missing", + "--cov-fail-under=100", +] diff --git a/packages/offer-approval/tests/test_packet.py b/packages/offer-approval/tests/test_packet.py new file mode 100644 index 000000000..dd09bb667 --- /dev/null +++ b/packages/offer-approval/tests/test_packet.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +from dataclasses import FrozenInstanceError, replace +from datetime import datetime, timedelta, timezone, tzinfo +from hashlib import sha256 +import json + +import pytest + +from orgmetra_offer_approval import ( + OfferApprovalPacket, + build_offer_approval_packet, +) + + +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 + + +def valid_kwargs() -> dict[str, object]: + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "offer_approval_reference": "offer_approval:offer-001", + "candidate_profile_reference": "candidate_profile:candidate-001", + "requisition_reference": "requisition:req-001", + "job_profile_reference": "job_profile:job-001", + "position_record_reference": "position_record:position-001", + "selection_decision_reference": "selection_decision:decision-001", + "selection_decision_digest": DIGEST_A, + "compensation_package_reference": "compensation_package:package-001", + "compensation_package_digest": DIGEST_B, + "offer_terms_reference": "offer_terms:terms-001", + "offer_terms_digest": DIGEST_C, + "requester_reference": "actor:requester-001", + "approver_reference": "actor:approver-001", + "purpose_code": "offer_approval_review", + "reason_code": "selected_candidate_offer_review", + "generated_at": datetime(2026, 8, 19, 5, 10, 0, 123456, tzinfo=timezone.utc), + } + + +def build_valid() -> OfferApprovalPacket: + return build_offer_approval_packet(**valid_kwargs()) + + +def test_builds_value_free_human_offer_approval_packet() -> None: + packet = build_valid() + + assert packet.contains_candidate_pii is False + assert packet.contains_compensation_values is False + assert packet.human_confirmation_required is True + assert packet.decision_authority == "human_approval_only" + assert packet.review_state == "requires_human_approval" + assert packet.delivery_state == "not_authorized_to_send" + assert "authoritative offer workflow" in packet.next_action + assert "communicating or executing the offer" in packet.next_action + + +def test_position_reference_is_optional_without_collapsing_job_scope() -> None: + kwargs = valid_kwargs() + kwargs["position_record_reference"] = None + packet = build_offer_approval_packet(**kwargs) + payload = json.loads(packet.canonical_json()) + + assert packet.job_profile_reference == "job_profile:job-001" + assert payload["position_record_reference"] is None + + +def test_canonical_json_and_digest_are_deterministic_and_value_free() -> None: + packet = build_valid() + payload = json.loads(packet.canonical_json()) + + assert payload["generated_at"] == "2026-08-19T05:10:00.123456Z" + assert payload["candidate_profile_reference"] == "candidate_profile:candidate-001" + assert "candidate_name" not in payload + assert "candidate_email" not in payload + assert "salary" not in payload + assert "compensation_value" not in payload + assert "assessment_score" not in payload + assert "model_output" not in payload + assert packet.sha256_digest() == sha256(packet.canonical_json().encode("utf-8")).hexdigest() + + +def test_fractional_seconds_remain_distinct_evidence() -> None: + first = build_valid() + second = replace(first, generated_at=first.generated_at + timedelta(microseconds=1)) + + assert first.canonical_json() != second.canonical_json() + assert first.sha256_digest() != second.sha256_digest() + + +@pytest.mark.parametrize( + ("field_name", "value"), + [ + ("tenant_record_id", "not-a-uuid"), + ("tenant_record_id", "00000000-0000-0000-0000-000000000000"), + ("tenant_record_id", "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"), + ("tenant_record_id", None), + ], +) +def test_rejects_nonoperational_tenant_identity(field_name: str, value: object) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match="tenant_record_id"): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("offer_approval_reference", "offer:offer-001", "offer_approval"), + ("candidate_profile_reference", "candidate:candidate-001", "candidate_profile"), + ("requisition_reference", "request:req-001", "requisition"), + ("job_profile_reference", "job:job-001", "job_profile"), + ("position_record_reference", "position:position-001", "position_record"), + ("selection_decision_reference", "decision:decision-001", "selection_decision"), + ("compensation_package_reference", "compensation:package-001", "compensation_package"), + ("offer_terms_reference", "terms:terms-001", "offer_terms"), + ("requester_reference", "person:requester-001", "actor"), + ("approver_reference", "reviewer:approver-001", "actor"), + ("requester_reference", "actor:", "actor"), + ("requester_reference", 1, "actor"), + ("requester_reference", "actor:" + "a" * 155, "actor"), + ], +) +def test_rejects_bad_opaque_references( + field_name: str, + value: object, + message: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + "field_name", + [ + "selection_decision_digest", + "compensation_package_digest", + "offer_terms_digest", + ], +) +@pytest.mark.parametrize("value", ["A" * 64, "a" * 63, 1]) +def test_rejects_malformed_digests(field_name: str, value: object) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match="lowercase SHA-256"): + build_offer_approval_packet(**kwargs) + + +def test_approver_must_be_distinct_from_requester() -> None: + kwargs = valid_kwargs() + kwargs["approver_reference"] = kwargs["requester_reference"] + with pytest.raises(ValueError, match="different accountable actor"): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("purpose_code", "selection_review", "offer_approval_review"), + ("purpose_code", "OfferApprovalReview", "lower snake_case"), + ("purpose_code", "a_" + "b" * 64, "lower snake_case"), + ("purpose_code", 1, "lower snake_case"), + ("reason_code", "offer", "lower snake_case"), + ("reason_code", "Offer_Review", "lower snake_case"), + ("reason_code", 1, "lower snake_case"), + ], +) +def test_rejects_bad_governance_codes( + field_name: str, + value: object, + message: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + build_offer_approval_packet(**kwargs) + + +class NullOffsetTz(tzinfo): + def utcoffset(self, dt: datetime | None) -> None: + return None + + def dst(self, dt: datetime | None) -> None: + return None + + def tzname(self, dt: datetime | None) -> str: + return "NULL" + + +@pytest.mark.parametrize( + "value", + [ + datetime(2026, 8, 19, 5, 10), + "2026-08-19T05:10:00Z", + 1, + datetime(2026, 8, 19, 5, 10).replace(tzinfo=NullOffsetTz()), + ], +) +def test_rejects_nonaware_generation_time(value: object) -> None: + kwargs = valid_kwargs() + kwargs["generated_at"] = value + with pytest.raises(ValueError, match="timezone-aware"): + build_offer_approval_packet(**kwargs) + + +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("contains_candidate_pii", True, "candidate PII"), + ("contains_candidate_pii", 0, "candidate PII"), + ("contains_compensation_values", True, "compensation values"), + ("contains_compensation_values", 0, "compensation values"), + ("human_confirmation_required", False, "human confirmation"), + ("human_confirmation_required", 1, "human confirmation"), + ("decision_authority", "automated", "human_approval_only"), + ("review_state", "approved", "requires_human_approval"), + ("delivery_state", "ready_to_send", "not_authorized_to_send"), + ("next_action", "Send the offer.", "governed offer-approval instruction"), + ], +) +def test_direct_constructor_and_replace_fail_closed( + field_name: str, + value: object, + message: str, +) -> None: + packet = build_valid() + with pytest.raises(ValueError, match=message): + replace(packet, **{field_name: value}) + + +def test_frozen_packet_rejects_mutation() -> None: + packet = build_valid() + with pytest.raises(FrozenInstanceError): + packet.review_state = "approved" + + +def test_timezone_is_normalized_without_losing_precision() -> None: + kwargs = valid_kwargs() + kwargs["generated_at"] = datetime( + 2026, + 8, + 19, + 14, + 10, + 0, + 654321, + tzinfo=timezone(timedelta(hours=9)), + ) + packet = build_offer_approval_packet(**kwargs) + + payload = json.loads(packet.canonical_json()) + assert payload["generated_at"] == "2026-08-19T05:10:00.654321Z" From f945bff710b203e2d8f44102d1f9d2ecbe0a063a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:10:01 -0700 Subject: [PATCH 02/49] feat: implement governed offer approval packet --- docs/adr/0017-governed-offer-approval.md | 59 +++++ docs/doctoring/offer-approval-references.md | 22 ++ docs/traceability/offer-approval.md | 18 ++ packages/offer-approval/CHANGELOG.md | 9 + packages/offer-approval/README.md | 51 ++++ .../src/orgmetra_offer_approval/__init__.py | 5 + .../src/orgmetra_offer_approval/packet.py | 237 ++++++++++++++++++ 7 files changed, 401 insertions(+) create mode 100644 docs/adr/0017-governed-offer-approval.md create mode 100644 docs/doctoring/offer-approval-references.md create mode 100644 docs/traceability/offer-approval.md create mode 100644 packages/offer-approval/CHANGELOG.md create mode 100644 packages/offer-approval/README.md create mode 100644 packages/offer-approval/src/orgmetra_offer_approval/__init__.py create mode 100644 packages/offer-approval/src/orgmetra_offer_approval/packet.py diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md new file mode 100644 index 000000000..0ad6f8df8 --- /dev/null +++ b/docs/adr/0017-governed-offer-approval.md @@ -0,0 +1,59 @@ +# ADR 0017: Governed offer approval evidence + +- Status: Proposed — active PR only +- Date: 2026-08-19 +- Scope: Talent Acquisition offer review + +## Context + +Protected `develop` can govern candidate, requisition, selection, and employment evidence, +but it does not yet expose a bounded pre-send contract proving that a proposed offer is tied +to the selected candidate, authoritative Job/optional Position, reviewed selection decision, +compensation-package provenance, offer-terms provenance, and accountable human approval. + +Offer review is high-impact employment workflow. A governance envelope must not become an +alternate decision authority, a salary-value cache, or a channel that lets generated/model +material masquerade as an approved offer. + +ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, +stakeholder management, and review. EEOC guidance on tests and selection procedures emphasizes +job-related use and employer responsibility for selection procedures. Those sources support a +conservative evidence-and-human-review boundary; they do not by themselves certify this package +or decide the legality of any offer. + +## Decision + +Orgmetra will expose `OfferApprovalPacket` as value-free review evidence only. + +The packet binds opaque references for the candidate profile, requisition, Job, optional +Position, selection decision, compensation package, and offer terms. Decision/package/terms +artifacts are independently SHA-256 bound. Requester and approver must be different actors. + +The packet must not contain candidate PII, compensation values, assessment scores, or +free-form model output. Direct construction and `dataclasses.replace(...)` revalidate all +trust-bearing invariants. + +Every packet is fixed to: + +- purpose `offer_approval_review`; +- `human_confirmation_required=True`; +- decision authority `human_approval_only`; +- review state `requires_human_approval`; +- delivery state `not_authorized_to_send`. + +Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does not approve, +communicate, send, execute, or persist an offer. + +## Consequences + +A buyer can review one deterministic, PII-minimized envelope before an offer moves to the +authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, +while Orgmetra keeps exact provenance references and human accountability. + +Downstream offer persistence/execution must independently enforce authorization, evidence +versioning, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains +proposed active-PR truth until integrated into protected `develop`. + +## References + +See `docs/doctoring/offer-approval-references.md`. diff --git a/docs/doctoring/offer-approval-references.md b/docs/doctoring/offer-approval-references.md new file mode 100644 index 000000000..b0de5316a --- /dev/null +++ b/docs/doctoring/offer-approval-references.md @@ -0,0 +1,22 @@ +# Offer approval references + +Retrieved August 19, 2026. + +International Organization for Standardization. (2023). *ISO 30405:2023 Human resource +management—Guidelines on recruitment* (2nd ed.). https://www.iso.org/standard/79488.html + +U.S. Equal Employment Opportunity Commission. (2007, December 1). *Employment tests and +selection procedures*. https://www.eeoc.gov/laws/guidance/employment-tests-and-selection-procedures + +U.S. Equal Employment Opportunity Commission, U.S. Department of Justice, U.S. Department of +Labor, U.S. Office of Personnel Management, & U.S. Department of the Treasury. (1979, March 1). +*Questions and answers to clarify and provide a common interpretation of the Uniform Guidelines +on Employee Selection Procedures*. https://www.eeoc.gov/laws/guidance/questions-and-answers-clarify-and-provide-common-interpretation-uniform-guidelines + +## Applied boundary + +These sources support recruitment-process governance, job-related selection responsibility, +stakeholder review, and documented use of selection evidence. This package makes no +certification or legal-compliance claim. It deliberately keeps candidate and compensation values +outside the governance envelope and requires accountable human approval before any offer may be +communicated or executed. diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md new file mode 100644 index 000000000..4cd624820 --- /dev/null +++ b/docs/traceability/offer-approval.md @@ -0,0 +1,18 @@ +# Governed offer approval traceability + +Status: **active PR / proposed capability**, not protected-main truth. + +| Buyer requirement | Executable evidence | Contract outcome | +| --- | --- | --- | +| Exact selected-candidate scope | `test_rejects_bad_opaque_references`; canonical JSON test | Candidate is correlated only by a bounded opaque `candidate_profile:` reference. | +| Separate Job and Position | valid packet + optional-Position test | Job is mandatory; Position is separately named and optional rather than collapsed into Job. | +| Reviewed selection evidence | digest/reference validation tests | Selection decision identity and SHA-256 evidence are required. | +| Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | +| Human accountability | distinct-actor test; immutable-state tests | Requester and approver differ; human approval is mandatory. | +| No premature offer delivery | direct-constructor/replace fail-closed tests | State remains `requires_human_approval` and `not_authorized_to_send`. | +| Deterministic audit correlation | canonical JSON, fractional-second, timezone, SHA-256 tests | Canonical evidence is precision-preserving and deterministic. | +| Public API readability | module/class/function docstrings | Beginner-readable contract boundary is documented in source and package README. | + +The SHA-256 packet digest proves only the exact canonical envelope bytes. It does not prove that +referenced evidence is substantively correct, that compensation is lawful/fair, that a human +approved the offer, or that an offer was delivered. diff --git a/packages/offer-approval/CHANGELOG.md b/packages/offer-approval/CHANGELOG.md new file mode 100644 index 000000000..3a307dd48 --- /dev/null +++ b/packages/offer-approval/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +## Unreleased + +- Add a governed, value-free pre-send offer approval packet. +- Require separate requester and approver identities and exact human approval. +- Bind selected-candidate, Job/optional Position, selection-decision, compensation-package, + and offer-terms provenance without copying candidate or compensation values. +- Keep every packet `requires_human_approval` and `not_authorized_to_send`. diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md new file mode 100644 index 000000000..ab1993ec6 --- /dev/null +++ b/packages/offer-approval/README.md @@ -0,0 +1,51 @@ +# Orgmetra governed offer approval + +This package creates a **value-free pre-send offer approval packet**. It is a governance +envelope, not an offer engine and not an employment decision. + +The packet binds one selected candidate to the exact requisition and authoritative Job, +an optional exact Position, the reviewed selection-decision digest, compensation-package +provenance, offer-terms provenance, and two accountable actors. The requester and approver +must be different. + +The envelope intentionally excludes candidate names, email addresses, demographic values, +assessment scores, salary/benefit amounts, credentials, and free-form model output. +`candidate_profile_reference` remains sensitive correlating metadata even though it is +opaque. + +A valid packet always remains `requires_human_approval` and +`not_authorized_to_send`. The next action is to verify Job/Position scope, selected-candidate +evidence, compensation-package provenance, and offer-terms provenance, then record +accountable human approval through the authoritative offer workflow before communicating +or executing the offer. + +Canonical JSON and its SHA-256 digest support immutable audit correlation. They do not prove +that the referenced evidence is true, that compensation is lawful or fair, that an offer +was approved, or that an offer was communicated. + +## Example + +```python +from datetime import datetime, timezone +from orgmetra_offer_approval import build_offer_approval_packet + +packet = build_offer_approval_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + offer_approval_reference="offer_approval:offer-001", + candidate_profile_reference="candidate_profile:candidate-001", + requisition_reference="requisition:req-001", + job_profile_reference="job_profile:job-001", + position_record_reference="position_record:position-001", + selection_decision_reference="selection_decision:decision-001", + selection_decision_digest="a" * 64, + compensation_package_reference="compensation_package:package-001", + compensation_package_digest="b" * 64, + offer_terms_reference="offer_terms:terms-001", + offer_terms_digest="c" * 64, + requester_reference="actor:requester-001", + approver_reference="actor:approver-001", + purpose_code="offer_approval_review", + reason_code="selected_candidate_offer_review", + generated_at=datetime.now(timezone.utc), +) +``` diff --git a/packages/offer-approval/src/orgmetra_offer_approval/__init__.py b/packages/offer-approval/src/orgmetra_offer_approval/__init__.py new file mode 100644 index 000000000..d8f99a53b --- /dev/null +++ b/packages/offer-approval/src/orgmetra_offer_approval/__init__.py @@ -0,0 +1,5 @@ +"""Public governed offer-approval contract.""" + +from .packet import OfferApprovalPacket, build_offer_approval_packet + +__all__ = ["OfferApprovalPacket", "build_offer_approval_packet"] diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py new file mode 100644 index 000000000..e20898eab --- /dev/null +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -0,0 +1,237 @@ +"""Governed, value-free human offer-approval evidence. + +The packet binds one selected candidate to an authoritative requisition and Job, an +optional exact Position, the reviewed selection decision, compensation-package +provenance, offer-terms provenance, and accountable human actors. The opaque candidate +reference remains sensitive correlating metadata. Candidate PII, compensation values, +assessment scores, and free-form model output remain outside this envelope. +""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from hashlib import sha256 +import json +import re +from uuid import UUID + +_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$") +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REFERENCE_PATTERN = re.compile( + r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" +) +_PURPOSE_CODE = "offer_approval_review" +_DECISION_AUTHORITY = "human_approval_only" +_REVIEW_STATE = "requires_human_approval" +_DELIVERY_STATE = "not_authorized_to_send" +_NEXT_ACTION = ( + "Verify authoritative Job/Position scope, selected-candidate evidence, " + "compensation-package provenance, and offer-terms provenance; then record accountable " + "human approval through the authoritative offer workflow before communicating or " + "executing the offer." +) + + +def _validate_operational_uuid(value: str, field_name: str) -> None: + """Require canonical non-sentinel UUID text for a governance identity.""" + try: + parsed = UUID(value) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(f"{field_name} must be canonical UUID text") from exc + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") + + +def _validate_code(value: str, field_name: str) -> None: + """Require a bounded descriptive lower snake_case governance code.""" + if not isinstance(value, str) or len(value) > 64 or not _CODE_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be bounded two-or-more-word lower snake_case") + + +def _validate_reference(value: str, prefix: str, field_name: str) -> None: + """Require a bounded namespaced opaque reference with the expected prefix.""" + if ( + not isinstance(value, str) + or len(value) > 160 + or not _REFERENCE_PATTERN.fullmatch(value) + or not value.startswith(f"{prefix}:") + ): + raise ValueError(f"{field_name} must be an opaque {prefix}: reference") + + +def _validate_digest(value: str, field_name: str) -> None: + """Require lowercase SHA-256 hexadecimal evidence.""" + if not isinstance(value, str) or not _DIGEST_PATTERN.fullmatch(value): + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _canonical_timestamp(value: datetime) -> str: + """Render an aware instant as precision-preserving UTC RFC 3339 text.""" + if not isinstance(value, datetime) or value.tzinfo is None or value.utcoffset() is None: + raise ValueError("generated_at must be timezone-aware") + return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") + + +@dataclass(frozen=True, slots=True) +class OfferApprovalPacket: + """Immutable value-free offer review packet awaiting accountable approval.""" + + tenant_record_id: str + offer_approval_reference: str + candidate_profile_reference: str + requisition_reference: str + job_profile_reference: str + position_record_reference: str | None + selection_decision_reference: str + selection_decision_digest: str + compensation_package_reference: str + compensation_package_digest: str + offer_terms_reference: str + offer_terms_digest: str + requester_reference: str + approver_reference: str + purpose_code: str + reason_code: str + generated_at: datetime + contains_candidate_pii: bool = False + contains_compensation_values: bool = False + human_confirmation_required: bool = True + decision_authority: str = _DECISION_AUTHORITY + review_state: str = _REVIEW_STATE + delivery_state: str = _DELIVERY_STATE + next_action: str = _NEXT_ACTION + + def __post_init__(self) -> None: + """Fail closed when direct construction drifts from the governed contract.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_reference( + self.offer_approval_reference, + "offer_approval", + "offer_approval_reference", + ) + _validate_reference( + self.candidate_profile_reference, + "candidate_profile", + "candidate_profile_reference", + ) + _validate_reference(self.requisition_reference, "requisition", "requisition_reference") + _validate_reference(self.job_profile_reference, "job_profile", "job_profile_reference") + if self.position_record_reference is not None: + _validate_reference( + self.position_record_reference, + "position_record", + "position_record_reference", + ) + _validate_reference( + self.selection_decision_reference, + "selection_decision", + "selection_decision_reference", + ) + _validate_digest(self.selection_decision_digest, "selection_decision_digest") + _validate_reference( + self.compensation_package_reference, + "compensation_package", + "compensation_package_reference", + ) + _validate_digest(self.compensation_package_digest, "compensation_package_digest") + _validate_reference(self.offer_terms_reference, "offer_terms", "offer_terms_reference") + _validate_digest(self.offer_terms_digest, "offer_terms_digest") + _validate_reference(self.requester_reference, "actor", "requester_reference") + _validate_reference(self.approver_reference, "actor", "approver_reference") + if self.requester_reference == self.approver_reference: + raise ValueError("approver_reference must identify a different accountable actor") + _validate_code(self.purpose_code, "purpose_code") + if self.purpose_code != _PURPOSE_CODE: + raise ValueError("purpose_code must remain offer_approval_review") + _validate_code(self.reason_code, "reason_code") + _canonical_timestamp(self.generated_at) + if self.contains_candidate_pii is not False: + raise ValueError("offer approval packet must not contain candidate PII") + if self.contains_compensation_values is not False: + raise ValueError("offer approval packet must not contain compensation values") + if self.human_confirmation_required is not True: + raise ValueError("human confirmation is mandatory before offer approval") + if self.decision_authority != _DECISION_AUTHORITY: + raise ValueError("decision_authority must remain human_approval_only") + if self.review_state != _REVIEW_STATE: + raise ValueError("review_state must remain requires_human_approval") + if self.delivery_state != _DELIVERY_STATE: + raise ValueError("delivery_state must remain not_authorized_to_send") + if self.next_action != _NEXT_ACTION: + raise ValueError("next_action must remain the governed offer-approval instruction") + + def canonical_json(self) -> str: + """Return deterministic canonical JSON for immutable audit correlation.""" + payload = { + "approver_reference": self.approver_reference, + "candidate_profile_reference": self.candidate_profile_reference, + "compensation_package_digest": self.compensation_package_digest, + "compensation_package_reference": self.compensation_package_reference, + "contains_candidate_pii": self.contains_candidate_pii, + "contains_compensation_values": self.contains_compensation_values, + "decision_authority": self.decision_authority, + "delivery_state": self.delivery_state, + "generated_at": _canonical_timestamp(self.generated_at), + "human_confirmation_required": self.human_confirmation_required, + "job_profile_reference": self.job_profile_reference, + "next_action": self.next_action, + "offer_approval_reference": self.offer_approval_reference, + "offer_terms_digest": self.offer_terms_digest, + "offer_terms_reference": self.offer_terms_reference, + "position_record_reference": self.position_record_reference, + "purpose_code": self.purpose_code, + "reason_code": self.reason_code, + "requester_reference": self.requester_reference, + "requisition_reference": self.requisition_reference, + "review_state": self.review_state, + "selection_decision_digest": self.selection_decision_digest, + "selection_decision_reference": self.selection_decision_reference, + "tenant_record_id": self.tenant_record_id, + } + return json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + + def sha256_digest(self) -> str: + """Return SHA-256 over the exact canonical UTF-8 offer-approval packet.""" + return sha256(self.canonical_json().encode("utf-8")).hexdigest() + + +def build_offer_approval_packet( + *, + tenant_record_id: str, + offer_approval_reference: str, + candidate_profile_reference: str, + requisition_reference: str, + job_profile_reference: str, + position_record_reference: str | None, + selection_decision_reference: str, + selection_decision_digest: str, + compensation_package_reference: str, + compensation_package_digest: str, + offer_terms_reference: str, + offer_terms_digest: str, + requester_reference: str, + approver_reference: str, + purpose_code: str, + reason_code: str, + generated_at: datetime, +) -> OfferApprovalPacket: + """Build value-free offer-approval evidence pending accountable human approval.""" + return OfferApprovalPacket( + tenant_record_id=tenant_record_id, + offer_approval_reference=offer_approval_reference, + candidate_profile_reference=candidate_profile_reference, + requisition_reference=requisition_reference, + job_profile_reference=job_profile_reference, + position_record_reference=position_record_reference, + selection_decision_reference=selection_decision_reference, + selection_decision_digest=selection_decision_digest, + compensation_package_reference=compensation_package_reference, + compensation_package_digest=compensation_package_digest, + offer_terms_reference=offer_terms_reference, + offer_terms_digest=offer_terms_digest, + requester_reference=requester_reference, + approver_reference=approver_reference, + purpose_code=purpose_code, + reason_code=reason_code, + generated_at=generated_at, + ) From b7294d8efac307a917d9e00499d38ebcefbf3aa5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:18:04 -0700 Subject: [PATCH 03/49] test: reject value-bearing offer references --- packages/offer-approval/tests/test_packet.py | 80 ++++++++++++++------ 1 file changed, 58 insertions(+), 22 deletions(-) diff --git a/packages/offer-approval/tests/test_packet.py b/packages/offer-approval/tests/test_packet.py index dd09bb667..dac11ca14 100644 --- a/packages/offer-approval/tests/test_packet.py +++ b/packages/offer-approval/tests/test_packet.py @@ -16,24 +16,34 @@ DIGEST_A = "a" * 64 DIGEST_B = "b" * 64 DIGEST_C = "c" * 64 +OFFER_ID = "10000000-0000-4000-8000-000000000001" +CANDIDATE_ID = "10000000-0000-4000-8000-000000000002" +REQUISITION_ID = "10000000-0000-4000-8000-000000000003" +JOB_ID = "10000000-0000-4000-8000-000000000004" +POSITION_ID = "10000000-0000-4000-8000-000000000005" +DECISION_ID = "10000000-0000-4000-8000-000000000006" +COMPENSATION_ID = "10000000-0000-4000-8000-000000000007" +TERMS_ID = "10000000-0000-4000-8000-000000000008" +REQUESTER_ID = "10000000-0000-4000-8000-000000000009" +APPROVER_ID = "10000000-0000-4000-8000-00000000000a" def valid_kwargs() -> dict[str, object]: return { "tenant_record_id": "11111111-1111-4111-8111-111111111111", - "offer_approval_reference": "offer_approval:offer-001", - "candidate_profile_reference": "candidate_profile:candidate-001", - "requisition_reference": "requisition:req-001", - "job_profile_reference": "job_profile:job-001", - "position_record_reference": "position_record:position-001", - "selection_decision_reference": "selection_decision:decision-001", + "offer_approval_reference": f"offer_approval:{OFFER_ID}", + "candidate_profile_reference": f"candidate_profile:{CANDIDATE_ID}", + "requisition_reference": f"requisition:{REQUISITION_ID}", + "job_profile_reference": f"job_profile:{JOB_ID}", + "position_record_reference": f"position_record:{POSITION_ID}", + "selection_decision_reference": f"selection_decision:{DECISION_ID}", "selection_decision_digest": DIGEST_A, - "compensation_package_reference": "compensation_package:package-001", + "compensation_package_reference": f"compensation_package:{COMPENSATION_ID}", "compensation_package_digest": DIGEST_B, - "offer_terms_reference": "offer_terms:terms-001", + "offer_terms_reference": f"offer_terms:{TERMS_ID}", "offer_terms_digest": DIGEST_C, - "requester_reference": "actor:requester-001", - "approver_reference": "actor:approver-001", + "requester_reference": f"actor:{REQUESTER_ID}", + "approver_reference": f"actor:{APPROVER_ID}", "purpose_code": "offer_approval_review", "reason_code": "selected_candidate_offer_review", "generated_at": datetime(2026, 8, 19, 5, 10, 0, 123456, tzinfo=timezone.utc), @@ -63,7 +73,7 @@ def test_position_reference_is_optional_without_collapsing_job_scope() -> None: packet = build_offer_approval_packet(**kwargs) payload = json.loads(packet.canonical_json()) - assert packet.job_profile_reference == "job_profile:job-001" + assert packet.job_profile_reference == f"job_profile:{JOB_ID}" assert payload["position_record_reference"] is None @@ -72,7 +82,7 @@ def test_canonical_json_and_digest_are_deterministic_and_value_free() -> None: payload = json.loads(packet.canonical_json()) assert payload["generated_at"] == "2026-08-19T05:10:00.123456Z" - assert payload["candidate_profile_reference"] == "candidate_profile:candidate-001" + assert payload["candidate_profile_reference"] == f"candidate_profile:{CANDIDATE_ID}" assert "candidate_name" not in payload assert "candidate_email" not in payload assert "salary" not in payload @@ -109,16 +119,16 @@ def test_rejects_nonoperational_tenant_identity(field_name: str, value: object) @pytest.mark.parametrize( ("field_name", "value", "message"), [ - ("offer_approval_reference", "offer:offer-001", "offer_approval"), - ("candidate_profile_reference", "candidate:candidate-001", "candidate_profile"), - ("requisition_reference", "request:req-001", "requisition"), - ("job_profile_reference", "job:job-001", "job_profile"), - ("position_record_reference", "position:position-001", "position_record"), - ("selection_decision_reference", "decision:decision-001", "selection_decision"), - ("compensation_package_reference", "compensation:package-001", "compensation_package"), - ("offer_terms_reference", "terms:terms-001", "offer_terms"), - ("requester_reference", "person:requester-001", "actor"), - ("approver_reference", "reviewer:approver-001", "actor"), + ("offer_approval_reference", f"offer:{OFFER_ID}", "offer_approval"), + ("candidate_profile_reference", f"candidate:{CANDIDATE_ID}", "candidate_profile"), + ("requisition_reference", f"request:{REQUISITION_ID}", "requisition"), + ("job_profile_reference", f"job:{JOB_ID}", "job_profile"), + ("position_record_reference", f"position:{POSITION_ID}", "position_record"), + ("selection_decision_reference", f"decision:{DECISION_ID}", "selection_decision"), + ("compensation_package_reference", f"compensation:{COMPENSATION_ID}", "compensation_package"), + ("offer_terms_reference", f"terms:{TERMS_ID}", "offer_terms"), + ("requester_reference", f"person:{REQUESTER_ID}", "actor"), + ("approver_reference", f"reviewer:{APPROVER_ID}", "actor"), ("requester_reference", "actor:", "actor"), ("requester_reference", 1, "actor"), ("requester_reference", "actor:" + "a" * 155, "actor"), @@ -135,6 +145,32 @@ def test_rejects_bad_opaque_references( build_offer_approval_packet(**kwargs) +@pytest.mark.parametrize( + ("field_name", "value", "message"), + [ + ("candidate_profile_reference", "candidate_profile:Jane-Doe", "opaque candidate_profile"), + ("compensation_package_reference", "compensation_package:120000", "opaque compensation_package"), + ("offer_terms_reference", "offer_terms:remote-two-days", "opaque offer_terms"), + ("requester_reference", "actor:seonghobae", "opaque actor"), + ("candidate_profile_reference", "candidate_profile:00000000-0000-0000-0000-000000000000", "opaque candidate_profile"), + ("candidate_profile_reference", "candidate_profile:FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", "opaque candidate_profile"), + ], +) +def test_rejects_value_bearing_or_noncanonical_reference_suffixes( + field_name: str, + value: object, + message: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + build_offer_approval_packet(**kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match=message): + replace(packet, **{field_name: value}) + + @pytest.mark.parametrize( "field_name", [ From 2ae811748aaf3dcbf7efd2f6d44530e037cce838 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:24:12 -0700 Subject: [PATCH 04/49] fix: require opaque UUID offer references --- .../src/orgmetra_offer_approval/packet.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index e20898eab..e81f8c153 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -49,14 +49,22 @@ def _validate_code(value: str, field_name: str) -> None: def _validate_reference(value: str, prefix: str, field_name: str) -> None: - """Require a bounded namespaced opaque reference with the expected prefix.""" + """Require an expected namespace plus a canonical operational UUID suffix.""" + error_message = f"{field_name} must be an opaque {prefix}: reference" if ( not isinstance(value, str) or len(value) > 160 or not _REFERENCE_PATTERN.fullmatch(value) or not value.startswith(f"{prefix}:") ): - raise ValueError(f"{field_name} must be an opaque {prefix}: reference") + raise ValueError(error_message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as exc: + raise ValueError(error_message) from exc + if str(parsed) != suffix or parsed.int in (0, (1 << 128) - 1): + raise ValueError(error_message) def _validate_digest(value: str, field_name: str) -> None: From c4539f4c5c29adc4aff288af0936801b03211236 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:25:42 -0700 Subject: [PATCH 05/49] docs: align offer references with opaque UUID contract --- packages/offer-approval/README.md | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index ab1993ec6..708202057 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -11,7 +11,9 @@ must be different. The envelope intentionally excludes candidate names, email addresses, demographic values, assessment scores, salary/benefit amounts, credentials, and free-form model output. `candidate_profile_reference` remains sensitive correlating metadata even though it is -opaque. +opaque. Every namespaced reference uses a canonical, non-sentinel UUID suffix; human-readable +or value-bearing suffixes are rejected so names, compensation values, offer terms, and actor +identities cannot be smuggled into the governance envelope through a reference field. A valid packet always remains `requires_human_approval` and `not_authorized_to_send`. The next action is to verify Job/Position scope, selected-candidate @@ -31,19 +33,19 @@ from orgmetra_offer_approval import build_offer_approval_packet packet = build_offer_approval_packet( tenant_record_id="11111111-1111-4111-8111-111111111111", - offer_approval_reference="offer_approval:offer-001", - candidate_profile_reference="candidate_profile:candidate-001", - requisition_reference="requisition:req-001", - job_profile_reference="job_profile:job-001", - position_record_reference="position_record:position-001", - selection_decision_reference="selection_decision:decision-001", + offer_approval_reference="offer_approval:10000000-0000-4000-8000-000000000001", + candidate_profile_reference="candidate_profile:10000000-0000-4000-8000-000000000002", + requisition_reference="requisition:10000000-0000-4000-8000-000000000003", + job_profile_reference="job_profile:10000000-0000-4000-8000-000000000004", + position_record_reference="position_record:10000000-0000-4000-8000-000000000005", + selection_decision_reference="selection_decision:10000000-0000-4000-8000-000000000006", selection_decision_digest="a" * 64, - compensation_package_reference="compensation_package:package-001", + compensation_package_reference="compensation_package:10000000-0000-4000-8000-000000000007", compensation_package_digest="b" * 64, - offer_terms_reference="offer_terms:terms-001", + offer_terms_reference="offer_terms:10000000-0000-4000-8000-000000000008", offer_terms_digest="c" * 64, - requester_reference="actor:requester-001", - approver_reference="actor:approver-001", + requester_reference="actor:10000000-0000-4000-8000-000000000009", + approver_reference="actor:10000000-0000-4000-8000-00000000000a", purpose_code="offer_approval_review", reason_code="selected_candidate_offer_review", generated_at=datetime.now(timezone.utc), From 4917c06f6db4ab5f1c5d4f0d9c2f97efed89552c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:14:50 -0700 Subject: [PATCH 06/49] test: require authoritative offer actor separation --- .../tests/test_actor_separation.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 packages/offer-approval/tests/test_actor_separation.py diff --git a/packages/offer-approval/tests/test_actor_separation.py b/packages/offer-approval/tests/test_actor_separation.py new file mode 100644 index 000000000..1618d6e62 --- /dev/null +++ b/packages/offer-approval/tests/test_actor_separation.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest + +from orgmetra_offer_approval import build_offer_approval_packet + + +def _build(**overrides): + values = { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "offer_approval_reference": "offer_approval:22222222-2222-4222-8222-222222222222", + "candidate_profile_reference": "candidate_profile:33333333-3333-4333-8333-333333333333", + "requisition_reference": "requisition:44444444-4444-4444-8444-444444444444", + "job_profile_reference": "job_profile:55555555-5555-4555-8555-555555555555", + "position_record_reference": None, + "selection_decision_reference": "selection_decision:66666666-6666-4666-8666-666666666666", + "selection_decision_digest": "a" * 64, + "compensation_package_reference": "compensation_package:77777777-7777-4777-8777-777777777777", + "compensation_package_digest": "b" * 64, + "offer_terms_reference": "offer_terms:88888888-8888-4888-8888-888888888888", + "offer_terms_digest": "c" * 64, + "requester_reference": "actor:99999999-9999-4999-8999-999999999999", + "approver_reference": "actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", + "purpose_code": "offer_approval_review", + "reason_code": "approved_offer_terms", + "generated_at": datetime(2026, 8, 19, 2, 15, tzinfo=timezone.utc), + } + values.update(overrides) + return build_offer_approval_packet(**values) + + +def test_requester_and_approver_require_authoritative_actor_separation() -> None: + with pytest.raises(ValueError, match="different accountable actor"): + _build(approver_reference="actor:99999999-9999-4999-8999-999999999999") + + normalized_next_action = _build().next_action.lower() + assert "requester_reference and approver_reference" in normalized_next_action + assert "resolved actor identities are distinct" in normalized_next_action From 46f73fe9d94bf72ac7cd7ec3e4c40acc1bfcf097 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:15:23 -0700 Subject: [PATCH 07/49] fix: require authoritative offer actor separation --- .../src/orgmetra_offer_approval/packet.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index e81f8c153..475e27714 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -25,10 +25,12 @@ _REVIEW_STATE = "requires_human_approval" _DELIVERY_STATE = "not_authorized_to_send" _NEXT_ACTION = ( - "Verify authoritative Job/Position scope, selected-candidate evidence, " - "compensation-package provenance, and offer-terms provenance; then record accountable " - "human approval through the authoritative offer workflow before communicating or " - "executing the offer." + "Within tenant_record_id, re-resolve requester_reference and approver_reference through " + "the authoritative actor boundary and verify their resolved actor identities are " + "distinct; then verify authoritative Job/Position scope, selected-candidate evidence, " + "compensation-package provenance, and offer-terms provenance before recording " + "accountable human approval through the authoritative offer workflow and before " + "communicating or executing the offer." ) From 9a4783fc4ac9cf92f428e4d25b63ce2a1989b18d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:15:55 -0700 Subject: [PATCH 08/49] docs: require authoritative offer actor separation --- packages/offer-approval/README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index 708202057..7415e8bf4 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -5,8 +5,8 @@ envelope, not an offer engine and not an employment decision. The packet binds one selected candidate to the exact requisition and authoritative Job, an optional exact Position, the reviewed selection-decision digest, compensation-package -provenance, offer-terms provenance, and two accountable actors. The requester and approver -must be different. +provenance, offer-terms provenance, and two accountable actor references. Identical requester +and approver references are rejected as an early syntactic guard. The envelope intentionally excludes candidate names, email addresses, demographic values, assessment scores, salary/benefit amounts, credentials, and free-form model output. @@ -16,14 +16,17 @@ or value-bearing suffixes are rejected so names, compensation values, offer term identities cannot be smuggled into the governance envelope through a reference field. A valid packet always remains `requires_human_approval` and -`not_authorized_to_send`. The next action is to verify Job/Position scope, selected-candidate -evidence, compensation-package provenance, and offer-terms provenance, then record -accountable human approval through the authoritative offer workflow before communicating -or executing the offer. +`not_authorized_to_send`. Before approval, the host must re-resolve `requester_reference` and +`approver_reference` within the exact `tenant_record_id` through the authoritative actor +boundary and prove their resolved actor identities are distinct; opaque-reference inequality +alone is not separation-of-duties evidence. The host must then verify Job/Position scope, +selected-candidate evidence, compensation-package provenance, and offer-terms provenance before +recording accountable human approval through the authoritative offer workflow and before +communicating or executing the offer. Canonical JSON and its SHA-256 digest support immutable audit correlation. They do not prove -that the referenced evidence is true, that compensation is lawful or fair, that an offer -was approved, or that an offer was communicated. +that the referenced evidence is true, that actor identities are distinct, that compensation is +lawful or fair, that an offer was approved, or that an offer was communicated. ## Example From f0f8e753a646956a18dc05b22ee2c205f4b4cf26 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:16:08 -0700 Subject: [PATCH 09/49] docs: bind offer approval to resolved actors --- docs/adr/0017-governed-offer-approval.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index 0ad6f8df8..2ece5bbf1 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -13,7 +13,8 @@ compensation-package provenance, offer-terms provenance, and accountable human a Offer review is high-impact employment workflow. A governance envelope must not become an alternate decision authority, a salary-value cache, or a channel that lets generated/model -material masquerade as an approved offer. +material masquerade as an approved offer. Different opaque requester/approver references also +do not prove that the authoritative actor boundary resolves them to different people. ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, stakeholder management, and review. EEOC guidance on tests and selection procedures emphasizes @@ -27,7 +28,11 @@ Orgmetra will expose `OfferApprovalPacket` as value-free review evidence only. The packet binds opaque references for the candidate profile, requisition, Job, optional Position, selection decision, compensation package, and offer terms. Decision/package/terms -artifacts are independently SHA-256 bound. Requester and approver must be different actors. +artifacts are independently SHA-256 bound. Identical requester/approver references are rejected +as an early syntactic guard. Before approval, the host must re-resolve both actor references +within the exact packet tenant through the authoritative actor boundary and reject approval +unless the resolved actor identities are distinct. Reference inequality alone is not +separation-of-duties evidence. The packet must not contain candidate PII, compensation values, assessment scores, or free-form model output. Direct construction and `dataclasses.replace(...)` revalidate all @@ -42,13 +47,14 @@ Every packet is fixed to: - delivery state `not_authorized_to_send`. Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does not approve, -communicate, send, execute, or persist an offer. +communicate, send, execute, persist an offer, or prove authoritative actor identity. ## Consequences A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, -while Orgmetra keeps exact provenance references and human accountability. +while Orgmetra keeps exact provenance references and human accountability. Requester/approver +separation is proven only after tenant-scoped authoritative actor resolution. Downstream offer persistence/execution must independently enforce authorization, evidence versioning, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains From 1f03ef5cc128799b9b1ef665797c3a0ebc204f1b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:16:20 -0700 Subject: [PATCH 10/49] docs: trace authoritative offer actor separation --- docs/traceability/offer-approval.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md index 4cd624820..6ad69c753 100644 --- a/docs/traceability/offer-approval.md +++ b/docs/traceability/offer-approval.md @@ -8,11 +8,13 @@ Status: **active PR / proposed capability**, not protected-main truth. | Separate Job and Position | valid packet + optional-Position test | Job is mandatory; Position is separately named and optional rather than collapsed into Job. | | Reviewed selection evidence | digest/reference validation tests | Selection decision identity and SHA-256 evidence are required. | | Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | -| Human accountability | distinct-actor test; immutable-state tests | Requester and approver differ; human approval is mandatory. | +| Human accountability and separation of duties | same-reference rejection plus `test_actor_separation.py` | Requester/approver references differ locally, and approval requires tenant-scoped authoritative resolution proving distinct resolved actor identities. | | No premature offer delivery | direct-constructor/replace fail-closed tests | State remains `requires_human_approval` and `not_authorized_to_send`. | | Deterministic audit correlation | canonical JSON, fractional-second, timezone, SHA-256 tests | Canonical evidence is precision-preserving and deterministic. | | Public API readability | module/class/function docstrings | Beginner-readable contract boundary is documented in source and package README. | The SHA-256 packet digest proves only the exact canonical envelope bytes. It does not prove that -referenced evidence is substantively correct, that compensation is lawful/fair, that a human -approved the offer, or that an offer was delivered. +referenced evidence is substantively correct, that requester/approver resolve to different +identities, that compensation is lawful/fair, that a human approved the offer, or that an offer +was delivered. Authoritative actor resolution remains outside this evidence packet and is a +required pre-approval host check. From 193cfc9ed895be94a26b7fe77203881ca171a27b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:18:49 -0700 Subject: [PATCH 11/49] test: require redacted offer approval repr --- packages/offer-approval/tests/test_packet.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/offer-approval/tests/test_packet.py b/packages/offer-approval/tests/test_packet.py index dac11ca14..3e2810fd5 100644 --- a/packages/offer-approval/tests/test_packet.py +++ b/packages/offer-approval/tests/test_packet.py @@ -92,6 +92,19 @@ def test_canonical_json_and_digest_are_deterministic_and_value_free() -> None: assert packet.sha256_digest() == sha256(packet.canonical_json().encode("utf-8")).hexdigest() +def test_repr_redacts_candidate_compensation_and_actor_correlation() -> None: + packet = build_valid() + rendered = repr(packet) + + assert rendered == "OfferApprovalPacket()" + assert packet.tenant_record_id not in rendered + assert packet.candidate_profile_reference not in rendered + assert packet.compensation_package_reference not in rendered + assert packet.compensation_package_digest not in rendered + assert packet.requester_reference not in rendered + assert packet.approver_reference not in rendered + + def test_fractional_seconds_remain_distinct_evidence() -> None: first = build_valid() second = replace(first, generated_at=first.generated_at + timedelta(microseconds=1)) From 9336f05ebe4790289d366fe9a9cbcf4b8e6cda28 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:19:22 -0700 Subject: [PATCH 12/49] fix: redact offer approval evidence repr --- .../offer-approval/src/orgmetra_offer_approval/packet.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index 475e27714..15bdbcec1 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -82,7 +82,7 @@ def _canonical_timestamp(value: datetime) -> str: return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") -@dataclass(frozen=True, slots=True) +@dataclass(frozen=True, slots=True, repr=False) class OfferApprovalPacket: """Immutable value-free offer review packet awaiting accountable approval.""" @@ -111,6 +111,10 @@ class OfferApprovalPacket: delivery_state: str = _DELIVERY_STATE next_action: str = _NEXT_ACTION + def __repr__(self) -> str: + """Return a representation that never emits candidate or compensation evidence.""" + return "OfferApprovalPacket()" + def __post_init__(self) -> None: """Fail closed when direct construction drifts from the governed contract.""" _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") From f93ff95f3f1bb27a7262cf6ee9ebeccae448056d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:32:09 -0700 Subject: [PATCH 13/49] test: cover direct offer packet construction --- packages/offer-approval/tests/test_packet.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/offer-approval/tests/test_packet.py b/packages/offer-approval/tests/test_packet.py index 3e2810fd5..a0fb5ae0a 100644 --- a/packages/offer-approval/tests/test_packet.py +++ b/packages/offer-approval/tests/test_packet.py @@ -277,6 +277,11 @@ def test_direct_constructor_and_replace_fail_closed( value: object, message: str, ) -> None: + direct_kwargs = valid_kwargs() + direct_kwargs[field_name] = value + with pytest.raises(ValueError, match=message): + OfferApprovalPacket(**direct_kwargs) + packet = build_valid() with pytest.raises(ValueError, match=message): replace(packet, **{field_name: value}) From 21a0dbc36a9d4811f034f7756ce1816e2d327815 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:29:33 -0700 Subject: [PATCH 14/49] test: reject value-bearing offer reason codes --- packages/offer-approval/tests/test_packet.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/offer-approval/tests/test_packet.py b/packages/offer-approval/tests/test_packet.py index a0fb5ae0a..91b9f97ba 100644 --- a/packages/offer-approval/tests/test_packet.py +++ b/packages/offer-approval/tests/test_packet.py @@ -230,6 +230,25 @@ def test_rejects_bad_governance_codes( build_offer_approval_packet(**kwargs) +@pytest.mark.parametrize( + "value", + [ + "jane_doe", + "salary_120000", + "remote_two_days", + ], +) +def test_rejects_value_bearing_reason_codes_through_direct_and_replace(value: str) -> None: + kwargs = valid_kwargs() + kwargs["reason_code"] = value + with pytest.raises(ValueError, match="reviewed non-sensitive"): + OfferApprovalPacket(**kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match="reviewed non-sensitive"): + replace(packet, reason_code=value) + + class NullOffsetTz(tzinfo): def utcoffset(self, dt: datetime | None) -> None: return None From 04a3fa12769fced8a438852783678cd8bf08af37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:30:27 -0700 Subject: [PATCH 15/49] fix: close offer reason metadata vocabulary --- packages/offer-approval/src/orgmetra_offer_approval/packet.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index 15bdbcec1..3bfa2d9c4 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -21,6 +21,7 @@ r"^[a-z][a-z0-9_]{1,31}:[A-Za-z0-9](?:[A-Za-z0-9._-]{0,126}[A-Za-z0-9])?$" ) _PURPOSE_CODE = "offer_approval_review" +_ALLOWED_REASON_CODES = frozenset({"selected_candidate_offer_review"}) _DECISION_AUTHORITY = "human_approval_only" _REVIEW_STATE = "requires_human_approval" _DELIVERY_STATE = "not_authorized_to_send" @@ -158,6 +159,8 @@ def __post_init__(self) -> None: if self.purpose_code != _PURPOSE_CODE: raise ValueError("purpose_code must remain offer_approval_review") _validate_code(self.reason_code, "reason_code") + if self.reason_code not in _ALLOWED_REASON_CODES: + raise ValueError("reason_code must use a reviewed non-sensitive offer reason") _canonical_timestamp(self.generated_at) if self.contains_candidate_pii is not False: raise ValueError("offer approval packet must not contain candidate PII") From 372a8bd7c53edd648c2a55bb83b68293a04a2767 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:30:48 -0700 Subject: [PATCH 16/49] docs: document closed offer reason vocabulary --- packages/offer-approval/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index 7415e8bf4..f9ea22053 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -14,6 +14,9 @@ assessment scores, salary/benefit amounts, credentials, and free-form model outp opaque. Every namespaced reference uses a canonical, non-sentinel UUID suffix; human-readable or value-bearing suffixes are rejected so names, compensation values, offer terms, and actor identities cannot be smuggled into the governance envelope through a reference field. +`reason_code` is likewise closed to the reviewed, value-free +`selected_candidate_offer_review` code; arbitrary lower-snake-case text is rejected so the +reason field cannot become a side channel for candidate, compensation, or offer-term values. A valid packet always remains `requires_human_approval` and `not_authorized_to_send`. Before approval, the host must re-resolve `requester_reference` and From 172088cd0b2c39d1f33fcb5c83a3183d7264888f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:31:06 -0700 Subject: [PATCH 17/49] docs: bind offer reason to reviewed contract --- docs/adr/0017-governed-offer-approval.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index 2ece5bbf1..90cae511c 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -35,12 +35,15 @@ unless the resolved actor identities are distinct. Reference inequality alone is separation-of-duties evidence. The packet must not contain candidate PII, compensation values, assessment scores, or -free-form model output. Direct construction and `dataclasses.replace(...)` revalidate all -trust-bearing invariants. +free-form model output. The `reason_code` field is not free-form metadata: it is closed to the +reviewed value-free `selected_candidate_offer_review` code so syntactically valid text cannot +smuggle candidate, compensation, or offer-term values into canonical evidence. Direct +construction and `dataclasses.replace(...)` revalidate all trust-bearing invariants. Every packet is fixed to: - purpose `offer_approval_review`; +- reviewed reason `selected_candidate_offer_review`; - `human_confirmation_required=True`; - decision authority `human_approval_only`; - review state `requires_human_approval`; @@ -54,7 +57,9 @@ communicate, send, execute, persist an offer, or prove authoritative actor ident A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, while Orgmetra keeps exact provenance references and human accountability. Requester/approver -separation is proven only after tenant-scoped authoritative actor resolution. +separation is proven only after tenant-scoped authoritative actor resolution. New offer-review +reason categories require an explicit contract change and regression evidence rather than +accepting arbitrary caller text. Downstream offer persistence/execution must independently enforce authorization, evidence versioning, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains From 01dcbb3e73c3828a957722237826dcd40bc215b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:31:16 -0700 Subject: [PATCH 18/49] docs: trace offer reason minimization regression --- docs/traceability/offer-approval.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md index 6ad69c753..845b397d9 100644 --- a/docs/traceability/offer-approval.md +++ b/docs/traceability/offer-approval.md @@ -8,6 +8,7 @@ Status: **active PR / proposed capability**, not protected-main truth. | Separate Job and Position | valid packet + optional-Position test | Job is mandatory; Position is separately named and optional rather than collapsed into Job. | | Reviewed selection evidence | digest/reference validation tests | Selection decision identity and SHA-256 evidence are required. | | Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | +| Value-free reason metadata | `test_rejects_value_bearing_reason_codes_through_direct_and_replace` | `reason_code` is closed to reviewed `selected_candidate_offer_review`; arbitrary lower-snake-case candidate, compensation, or offer-term text fails closed. | | Human accountability and separation of duties | same-reference rejection plus `test_actor_separation.py` | Requester/approver references differ locally, and approval requires tenant-scoped authoritative resolution proving distinct resolved actor identities. | | No premature offer delivery | direct-constructor/replace fail-closed tests | State remains `requires_human_approval` and `not_authorized_to_send`. | | Deterministic audit correlation | canonical JSON, fractional-second, timezone, SHA-256 tests | Canonical evidence is precision-preserving and deterministic. | From 2ee0ece15e4af2484f085da4c9ef1bea5e0d5cf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:31:22 -0700 Subject: [PATCH 19/49] docs: record offer reason privacy hardening --- packages/offer-approval/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/offer-approval/CHANGELOG.md b/packages/offer-approval/CHANGELOG.md index 3a307dd48..ec0f462b6 100644 --- a/packages/offer-approval/CHANGELOG.md +++ b/packages/offer-approval/CHANGELOG.md @@ -6,4 +6,6 @@ - Require separate requester and approver identities and exact human approval. - Bind selected-candidate, Job/optional Position, selection-decision, compensation-package, and offer-terms provenance without copying candidate or compensation values. +- Close `reason_code` to the reviewed value-free `selected_candidate_offer_review` contract so + arbitrary candidate, compensation, or offer-term text cannot enter canonical evidence. - Keep every packet `requires_human_approval` and `not_authorized_to_send`. From 4d03ee2fcc08ded552e7281e71dfaa1786aa7e93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:07:17 -0700 Subject: [PATCH 20/49] fix: align actor-separation fixture with governed offer reason --- packages/offer-approval/tests/test_actor_separation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/offer-approval/tests/test_actor_separation.py b/packages/offer-approval/tests/test_actor_separation.py index 1618d6e62..e3f2e2df1 100644 --- a/packages/offer-approval/tests/test_actor_separation.py +++ b/packages/offer-approval/tests/test_actor_separation.py @@ -8,6 +8,7 @@ def _build(**overrides): + """Build a valid offer-approval packet, allowing one field to be varied by a regression.""" values = { "tenant_record_id": "11111111-1111-4111-8111-111111111111", "offer_approval_reference": "offer_approval:22222222-2222-4222-8222-222222222222", @@ -24,7 +25,7 @@ def _build(**overrides): "requester_reference": "actor:99999999-9999-4999-8999-999999999999", "approver_reference": "actor:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "purpose_code": "offer_approval_review", - "reason_code": "approved_offer_terms", + "reason_code": "selected_candidate_offer_review", "generated_at": datetime(2026, 8, 19, 2, 15, tzinfo=timezone.utc), } values.update(overrides) @@ -32,6 +33,7 @@ def _build(**overrides): def test_requester_and_approver_require_authoritative_actor_separation() -> None: + """Require distinct actor references plus authoritative identity separation before approval.""" with pytest.raises(ValueError, match="different accountable actor"): _build(approver_reference="actor:99999999-9999-4999-8999-999999999999") From 87ec227ba92dc4acb4e537ab90be2b008987a640 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:41:20 -0700 Subject: [PATCH 21/49] test: require offer approval evidence versioning --- .../tests/test_evidence_version.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/offer-approval/tests/test_evidence_version.py diff --git a/packages/offer-approval/tests/test_evidence_version.py b/packages/offer-approval/tests/test_evidence_version.py new file mode 100644 index 000000000..5ddc2a5e4 --- /dev/null +++ b/packages/offer-approval/tests/test_evidence_version.py @@ -0,0 +1,58 @@ +"""Regression coverage for high-impact offer-review evidence versioning.""" + +from dataclasses import replace +from datetime import datetime, timezone +import json + +import pytest + +from orgmetra_offer_approval import build_offer_approval_packet + + +def _build(evidence_version: object = 1): + """Build a valid offer-review packet while varying only its evidence version.""" + return build_offer_approval_packet( + tenant_record_id="11111111-1111-4111-8111-111111111111", + offer_approval_reference="offer_approval:10000000-0000-4000-8000-000000000001", + candidate_profile_reference="candidate_profile:10000000-0000-4000-8000-000000000002", + requisition_reference="requisition:10000000-0000-4000-8000-000000000003", + job_profile_reference="job_profile:10000000-0000-4000-8000-000000000004", + position_record_reference="position_record:10000000-0000-4000-8000-000000000005", + selection_decision_reference="selection_decision:10000000-0000-4000-8000-000000000006", + selection_decision_digest="a" * 64, + compensation_package_reference="compensation_package:10000000-0000-4000-8000-000000000007", + compensation_package_digest="b" * 64, + offer_terms_reference="offer_terms:10000000-0000-4000-8000-000000000008", + offer_terms_digest="c" * 64, + requester_reference="actor:10000000-0000-4000-8000-000000000009", + approver_reference="actor:10000000-0000-4000-8000-00000000000a", + purpose_code="offer_approval_review", + reason_code="selected_candidate_offer_review", + generated_at=datetime(2026, 8, 19, 5, 10, 0, 123456, tzinfo=timezone.utc), + evidence_version=evidence_version, + ) + + +def test_evidence_version_is_bound_to_offer_correlation_evidence() -> None: + """Changing evidence version must change canonical JSON and its correlation digest.""" + first = _build(1) + second = _build(2) + + assert first.evidence_version == 1 + assert json.loads(first.canonical_json())["evidence_version"] == 1 + assert second.evidence_version == 2 + assert first.canonical_json() != second.canonical_json() + assert first.sha256_digest() != second.sha256_digest() + + +@pytest.mark.parametrize("evidence_version", [0, -1, True, "1", 2_147_483_648]) +def test_evidence_version_fails_closed(evidence_version: object) -> None: + """Reject non-integer, non-positive, or overflow evidence versions.""" + with pytest.raises(ValueError, match="evidence_version"): + _build(evidence_version) + + +def test_replace_cannot_bypass_evidence_version_validation() -> None: + """Mutation-by-copy must revalidate the immutable evidence version invariant.""" + with pytest.raises(ValueError, match="evidence_version"): + replace(_build(), evidence_version=False) From ecf257b5f060781491a92146abbcdbd49615fc72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:41:53 -0700 Subject: [PATCH 22/49] fix: bind offer approval evidence version --- .../src/orgmetra_offer_approval/packet.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index 3bfa2d9c4..9d90eafab 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -83,6 +83,12 @@ def _canonical_timestamp(value: datetime) -> str: return value.astimezone(timezone.utc).isoformat().replace("+00:00", "Z") +def _validate_evidence_version(value: int) -> None: + """Require a bounded positive integer version for high-impact offer-review evidence.""" + if type(value) is not int or value < 1 or value > 2_147_483_647: + raise ValueError("evidence_version must be an integer from 1 through 2147483647") + + @dataclass(frozen=True, slots=True, repr=False) class OfferApprovalPacket: """Immutable value-free offer review packet awaiting accountable approval.""" @@ -104,6 +110,7 @@ class OfferApprovalPacket: purpose_code: str reason_code: str generated_at: datetime + evidence_version: int = 1 contains_candidate_pii: bool = False contains_compensation_values: bool = False human_confirmation_required: bool = True @@ -162,6 +169,7 @@ def __post_init__(self) -> None: if self.reason_code not in _ALLOWED_REASON_CODES: raise ValueError("reason_code must use a reviewed non-sensitive offer reason") _canonical_timestamp(self.generated_at) + _validate_evidence_version(self.evidence_version) if self.contains_candidate_pii is not False: raise ValueError("offer approval packet must not contain candidate PII") if self.contains_compensation_values is not False: @@ -188,6 +196,7 @@ def canonical_json(self) -> str: "contains_compensation_values": self.contains_compensation_values, "decision_authority": self.decision_authority, "delivery_state": self.delivery_state, + "evidence_version": self.evidence_version, "generated_at": _canonical_timestamp(self.generated_at), "human_confirmation_required": self.human_confirmation_required, "job_profile_reference": self.job_profile_reference, @@ -231,6 +240,7 @@ def build_offer_approval_packet( purpose_code: str, reason_code: str, generated_at: datetime, + evidence_version: int = 1, ) -> OfferApprovalPacket: """Build value-free offer-approval evidence pending accountable human approval.""" return OfferApprovalPacket( @@ -251,4 +261,5 @@ def build_offer_approval_packet( purpose_code=purpose_code, reason_code=reason_code, generated_at=generated_at, + evidence_version=evidence_version, ) From 35500daf132917e729c20cf619f88e528a0f5ba2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:42:28 -0700 Subject: [PATCH 23/49] docs: document offer evidence versioning --- packages/offer-approval/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index f9ea22053..24e3b2dd6 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -18,6 +18,12 @@ identities cannot be smuggled into the governance envelope through a reference f `selected_candidate_offer_review` code; arbitrary lower-snake-case text is rejected so the reason field cannot become a side channel for candidate, compensation, or offer-term values. +Every packet also carries a bounded positive integer `evidence_version` (default `1`). It is +serialized into canonical JSON, so changing the governed evidence version changes the packet +SHA-256 digest. Zero, negative, boolean, textual, and values above `2147483647` fail closed. +The field versions this immutable pre-send evidence envelope; it is not approval, delivery, +or proof that referenced source versions were authoritatively resolved. + A valid packet always remains `requires_human_approval` and `not_authorized_to_send`. Before approval, the host must re-resolve `requester_reference` and `approver_reference` within the exact `tenant_record_id` through the authoritative actor From 78719622c5b28c0259b1bb0a4de7b9ae06c2bfa3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:42:43 -0700 Subject: [PATCH 24/49] docs: bind evidence version in offer ADR --- docs/adr/0017-governed-offer-approval.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index 90cae511c..fa352d57d 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -44,11 +44,16 @@ Every packet is fixed to: - purpose `offer_approval_review`; - reviewed reason `selected_candidate_offer_review`; +- bounded positive integer `evidence_version` (default `1`), included in canonical JSON/SHA-256; - `human_confirmation_required=True`; - decision authority `human_approval_only`; - review state `requires_human_approval`; - delivery state `not_authorized_to_send`. +`evidence_version` accepts only real integers from `1` through `2147483647`; booleans, text, +zero, negative values, and overflow values fail closed. It versions the immutable pre-send +evidence envelope and does not itself prove source-version resolution, approval, or delivery. + Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does not approve, communicate, send, execute, persist an offer, or prove authoritative actor identity. @@ -56,13 +61,13 @@ communicate, send, execute, persist an offer, or prove authoritative actor ident A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, -while Orgmetra keeps exact provenance references and human accountability. Requester/approver -separation is proven only after tenant-scoped authoritative actor resolution. New offer-review -reason categories require an explicit contract change and regression evidence rather than -accepting arbitrary caller text. +while Orgmetra keeps exact provenance references, evidence version, and human accountability. +Requester/approver separation is proven only after tenant-scoped authoritative actor resolution. +New offer-review reason categories require an explicit contract change and regression evidence +rather than accepting arbitrary caller text. -Downstream offer persistence/execution must independently enforce authorization, evidence -versioning, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains +Downstream offer persistence/execution must independently enforce authorization, source-evidence +resolution, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains proposed active-PR truth until integrated into protected `develop`. ## References From 7f0df9029850fae78ecf67a53dbe8b0fc7e1eb1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:42:56 -0700 Subject: [PATCH 25/49] docs: trace offer evidence versioning --- docs/traceability/offer-approval.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md index 845b397d9..b4b492e29 100644 --- a/docs/traceability/offer-approval.md +++ b/docs/traceability/offer-approval.md @@ -10,12 +10,13 @@ Status: **active PR / proposed capability**, not protected-main truth. | Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | | Value-free reason metadata | `test_rejects_value_bearing_reason_codes_through_direct_and_replace` | `reason_code` is closed to reviewed `selected_candidate_offer_review`; arbitrary lower-snake-case candidate, compensation, or offer-term text fails closed. | | Human accountability and separation of duties | same-reference rejection plus `test_actor_separation.py` | Requester/approver references differ locally, and approval requires tenant-scoped authoritative resolution proving distinct resolved actor identities. | +| High-impact evidence versioning | `test_evidence_version.py` | Bounded positive `evidence_version` is in canonical JSON, changes correlation SHA-256 across versions, and revalidates through mutation-by-copy. | | No premature offer delivery | direct-constructor/replace fail-closed tests | State remains `requires_human_approval` and `not_authorized_to_send`. | -| Deterministic audit correlation | canonical JSON, fractional-second, timezone, SHA-256 tests | Canonical evidence is precision-preserving and deterministic. | +| Deterministic audit correlation | canonical JSON, fractional-second, timezone, evidence-version, SHA-256 tests | Canonical evidence is precision-preserving, versioned, and deterministic. | | Public API readability | module/class/function docstrings | Beginner-readable contract boundary is documented in source and package README. | The SHA-256 packet digest proves only the exact canonical envelope bytes. It does not prove that referenced evidence is substantively correct, that requester/approver resolve to different identities, that compensation is lawful/fair, that a human approved the offer, or that an offer -was delivered. Authoritative actor resolution remains outside this evidence packet and is a -required pre-approval host check. +was delivered. Authoritative actor and source-evidence resolution remain outside this evidence +packet and are required pre-approval host checks. From 0d57cbef07a59cd3961b398c9e2ede4c176e0ec9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:43:07 -0700 Subject: [PATCH 26/49] docs: record offer evidence versioning --- packages/offer-approval/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/offer-approval/CHANGELOG.md b/packages/offer-approval/CHANGELOG.md index ec0f462b6..b617a302d 100644 --- a/packages/offer-approval/CHANGELOG.md +++ b/packages/offer-approval/CHANGELOG.md @@ -8,4 +8,6 @@ and offer-terms provenance without copying candidate or compensation values. - Close `reason_code` to the reviewed value-free `selected_candidate_offer_review` contract so arbitrary candidate, compensation, or offer-term text cannot enter canonical evidence. +- Bind a bounded positive `evidence_version` into canonical JSON and SHA-256 correlation + evidence so high-impact offer-review evidence versions are explicit and fail closed. - Keep every packet `requires_human_approval` and `not_authorized_to_send`. From 4e47cc903ac8711319c83bf8f6230ed191799c6d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:27:10 -0700 Subject: [PATCH 27/49] test(offer-approval): require tenant-scoped reference resolution --- .../offer-approval/tests/test_actor_separation.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/offer-approval/tests/test_actor_separation.py b/packages/offer-approval/tests/test_actor_separation.py index e3f2e2df1..c9f04fd08 100644 --- a/packages/offer-approval/tests/test_actor_separation.py +++ b/packages/offer-approval/tests/test_actor_separation.py @@ -40,3 +40,17 @@ def test_requester_and_approver_require_authoritative_actor_separation() -> None normalized_next_action = _build().next_action.lower() assert "requester_reference and approver_reference" in normalized_next_action assert "resolved actor identities are distinct" in normalized_next_action + + +def test_approval_requires_every_reference_to_resolve_in_the_exact_tenant() -> None: + """Prevent cross-tenant offer evidence mixing behind syntactically valid UUID references.""" + action = _build().next_action + tenant_clause = "re-resolve every packet reference within tenant_record_id" + actor_clause = "verify their resolved actor identities are distinct" + scope_clause = "verify authoritative Job/Position scope" + approval_clause = "accountable human approval" + + assert tenant_clause in action + assert action.index(tenant_clause) < action.index(actor_clause) + assert action.index(actor_clause) < action.index(scope_clause) + assert action.index(scope_clause) < action.index(approval_clause) From f98d2c9206d8c925b344c582664cc8825da9cf7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:27:44 -0700 Subject: [PATCH 28/49] fix(offer-approval): bind all evidence to tenant scope --- .../src/orgmetra_offer_approval/packet.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index 9d90eafab..61461bb74 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -26,12 +26,12 @@ _REVIEW_STATE = "requires_human_approval" _DELIVERY_STATE = "not_authorized_to_send" _NEXT_ACTION = ( - "Within tenant_record_id, re-resolve requester_reference and approver_reference through " - "the authoritative actor boundary and verify their resolved actor identities are " - "distinct; then verify authoritative Job/Position scope, selected-candidate evidence, " - "compensation-package provenance, and offer-terms provenance before recording " - "accountable human approval through the authoritative offer workflow and before " - "communicating or executing the offer." + "Within tenant_record_id, re-resolve every packet reference through its authoritative " + "boundary; specifically re-resolve requester_reference and approver_reference and verify " + "their resolved actor identities are distinct; then verify authoritative Job/Position " + "scope, selected-candidate evidence, compensation-package provenance, and offer-terms " + "provenance before recording accountable human approval through the authoritative offer " + "workflow and before communicating or executing the offer." ) From 21a209fbae7b58fcddea95c35b50aa2148406b02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:28:10 -0700 Subject: [PATCH 29/49] docs(offer-approval): align tenant evidence boundary --- packages/offer-approval/README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index 24e3b2dd6..7773a0e95 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -25,17 +25,19 @@ The field versions this immutable pre-send evidence envelope; it is not approval or proof that referenced source versions were authoritatively resolved. A valid packet always remains `requires_human_approval` and -`not_authorized_to_send`. Before approval, the host must re-resolve `requester_reference` and -`approver_reference` within the exact `tenant_record_id` through the authoritative actor -boundary and prove their resolved actor identities are distinct; opaque-reference inequality -alone is not separation-of-duties evidence. The host must then verify Job/Position scope, -selected-candidate evidence, compensation-package provenance, and offer-terms provenance before -recording accountable human approval through the authoritative offer workflow and before -communicating or executing the offer. +`not_authorized_to_send`. Before approval, the host must re-resolve **every packet reference** +within the exact `tenant_record_id` through its authoritative boundary so valid UUIDs from a +foreign tenant cannot be mixed into the approval envelope. It must specifically re-resolve +`requester_reference` and `approver_reference` and prove their resolved actor identities are +distinct; opaque-reference inequality alone is not separation-of-duties evidence. The host +must then verify Job/Position scope, selected-candidate evidence, compensation-package +provenance, and offer-terms provenance before recording accountable human approval through the +authoritative offer workflow and before communicating or executing the offer. Canonical JSON and its SHA-256 digest support immutable audit correlation. They do not prove -that the referenced evidence is true, that actor identities are distinct, that compensation is -lawful or fair, that an offer was approved, or that an offer was communicated. +that the referenced evidence is true, that all references belong to the packet tenant, that +actor identities are distinct, that compensation is lawful or fair, that an offer was approved, +or that an offer was communicated. ## Example From cbf31d4224ee9d31d68f81d291a9fe2180875bad Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:28:31 -0700 Subject: [PATCH 30/49] docs(offer-approval): require exact-tenant evidence resolution --- docs/adr/0017-governed-offer-approval.md | 32 +++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index fa352d57d..d2b18b9a2 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -14,7 +14,9 @@ compensation-package provenance, offer-terms provenance, and accountable human a Offer review is high-impact employment workflow. A governance envelope must not become an alternate decision authority, a salary-value cache, or a channel that lets generated/model material masquerade as an approved offer. Different opaque requester/approver references also -do not prove that the authoritative actor boundary resolves them to different people. +do not prove that the authoritative actor boundary resolves them to different people, and +canonical UUID syntax does not prove that the referenced candidate, requisition, Job/Position, +selection decision, compensation package, or offer terms belong to the packet tenant. ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, stakeholder management, and review. EEOC guidance on tests and selection procedures emphasizes @@ -28,11 +30,12 @@ Orgmetra will expose `OfferApprovalPacket` as value-free review evidence only. The packet binds opaque references for the candidate profile, requisition, Job, optional Position, selection decision, compensation package, and offer terms. Decision/package/terms -artifacts are independently SHA-256 bound. Identical requester/approver references are rejected -as an early syntactic guard. Before approval, the host must re-resolve both actor references -within the exact packet tenant through the authoritative actor boundary and reject approval -unless the resolved actor identities are distinct. Reference inequality alone is not -separation-of-duties evidence. +artifacts are independently SHA-256 bound. Before approval, the host must re-resolve **every +packet reference** within the exact `tenant_record_id` through its authoritative boundary and +reject approval if any reference belongs to another tenant or cannot be authoritatively +resolved. Identical requester/approver references are rejected as an early syntactic guard; +after tenant-scoped resolution, the host must prove their resolved actor identities are +distinct. Reference inequality alone is not separation-of-duties evidence. The packet must not contain candidate PII, compensation values, assessment scores, or free-form model output. The `reason_code` field is not free-form metadata: it is closed to the @@ -55,20 +58,21 @@ zero, negative values, and overflow values fail closed. It versions the immutabl evidence envelope and does not itself prove source-version resolution, approval, or delivery. Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does not approve, -communicate, send, execute, persist an offer, or prove authoritative actor identity. +communicate, send, execute, persist an offer, or prove authoritative reference/actor identity. ## Consequences A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, while Orgmetra keeps exact provenance references, evidence version, and human accountability. -Requester/approver separation is proven only after tenant-scoped authoritative actor resolution. -New offer-review reason categories require an explicit contract change and regression evidence -rather than accepting arbitrary caller text. - -Downstream offer persistence/execution must independently enforce authorization, source-evidence -resolution, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains -proposed active-PR truth until integrated into protected `develop`. +Cross-tenant evidence mixing is fail-closed at the host approval boundary because every packet +reference must resolve in the exact tenant. Requester/approver separation is proven only after +tenant-scoped authoritative actor resolution. New offer-review reason categories require an +explicit contract change and regression evidence rather than accepting arbitrary caller text. + +Downstream offer persistence/execution must independently enforce authorization, tenant-scoped +source-evidence resolution, idempotency where applicable, and immutable audit/outbox evidence. +This ADR remains proposed active-PR truth until integrated into protected `develop`. ## References From 919225cbbcd15118879cd0f16450505b3b379c10 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:46:07 -0700 Subject: [PATCH 31/49] test(offer-approval): align tenant next-action assertion --- packages/offer-approval/tests/test_actor_separation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/offer-approval/tests/test_actor_separation.py b/packages/offer-approval/tests/test_actor_separation.py index c9f04fd08..00fbb40c8 100644 --- a/packages/offer-approval/tests/test_actor_separation.py +++ b/packages/offer-approval/tests/test_actor_separation.py @@ -45,7 +45,7 @@ def test_requester_and_approver_require_authoritative_actor_separation() -> None def test_approval_requires_every_reference_to_resolve_in_the_exact_tenant() -> None: """Prevent cross-tenant offer evidence mixing behind syntactically valid UUID references.""" action = _build().next_action - tenant_clause = "re-resolve every packet reference within tenant_record_id" + tenant_clause = "Within tenant_record_id, re-resolve every packet reference" actor_clause = "verify their resolved actor identities are distinct" scope_clause = "verify authoritative Job/Position scope" approval_clause = "accountable human approval" From ae51aa60a58dcf51fb17d6f58ff5daa780fe50b9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:29:44 -0700 Subject: [PATCH 32/49] test: reject UUIDv1 offer approval trust references --- packages/offer-approval/tests/test_packet.py | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/offer-approval/tests/test_packet.py b/packages/offer-approval/tests/test_packet.py index 91b9f97ba..c27e5832a 100644 --- a/packages/offer-approval/tests/test_packet.py +++ b/packages/offer-approval/tests/test_packet.py @@ -26,6 +26,7 @@ TERMS_ID = "10000000-0000-4000-8000-000000000008" REQUESTER_ID = "10000000-0000-4000-8000-000000000009" APPROVER_ID = "10000000-0000-4000-8000-00000000000a" +UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" def valid_kwargs() -> dict[str, object]: @@ -158,6 +159,35 @@ def test_rejects_bad_opaque_references( build_offer_approval_packet(**kwargs) +@pytest.mark.parametrize( + ("field_name", "prefix"), + [ + ("offer_approval_reference", "offer_approval"), + ("candidate_profile_reference", "candidate_profile"), + ("requisition_reference", "requisition"), + ("job_profile_reference", "job_profile"), + ("position_record_reference", "position_record"), + ("selection_decision_reference", "selection_decision"), + ("compensation_package_reference", "compensation_package"), + ("offer_terms_reference", "offer_terms"), + ("requester_reference", "actor"), + ("approver_reference", "actor"), + ], +) +def test_rejects_uuid1_trust_references_through_direct_and_replace( + field_name: str, + prefix: str, +) -> None: + kwargs = valid_kwargs() + kwargs[field_name] = f"{prefix}:{UUID1_ID}" + with pytest.raises(ValueError, match=f"opaque {prefix}"): + OfferApprovalPacket(**kwargs) + + packet = build_valid() + with pytest.raises(ValueError, match=f"opaque {prefix}"): + replace(packet, **{field_name: f"{prefix}:{UUID1_ID}"}) + + @pytest.mark.parametrize( ("field_name", "value", "message"), [ From 45ecd1001709388dc4f17e977be08cecd1938e8a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:30:13 -0700 Subject: [PATCH 33/49] fix: require UUIDv4 offer approval trust references --- packages/offer-approval/src/orgmetra_offer_approval/packet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index 61461bb74..37dd71310 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -52,7 +52,7 @@ def _validate_code(value: str, field_name: str) -> None: def _validate_reference(value: str, prefix: str, field_name: str) -> None: - """Require an expected namespace plus a canonical operational UUID suffix.""" + """Require an expected namespace plus a canonical opaque UUIDv4 suffix.""" error_message = f"{field_name} must be an opaque {prefix}: reference" if ( not isinstance(value, str) @@ -66,7 +66,7 @@ def _validate_reference(value: str, prefix: str, field_name: str) -> None: parsed = UUID(suffix) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(error_message) from exc - if str(parsed) != suffix or parsed.int in (0, (1 << 128) - 1): + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): raise ValueError(error_message) From 301dc3f002467b6cd29a50aa2328217069ef5dbf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:30:43 -0700 Subject: [PATCH 34/49] docs: define UUIDv4 offer reference privacy contract --- packages/offer-approval/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index 7773a0e95..71ed0af9b 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -11,10 +11,10 @@ and approver references are rejected as an early syntactic guard. The envelope intentionally excludes candidate names, email addresses, demographic values, assessment scores, salary/benefit amounts, credentials, and free-form model output. `candidate_profile_reference` remains sensitive correlating metadata even though it is -opaque. Every namespaced reference uses a canonical, non-sentinel UUID suffix; human-readable -or value-bearing suffixes are rejected so names, compensation values, offer terms, and actor -identities cannot be smuggled into the governance envelope through a reference field. -`reason_code` is likewise closed to the reviewed, value-free +opaque. Every namespaced reference uses a canonical, non-sentinel UUIDv4 suffix; UUIDv1 and +other UUID versions are rejected so timestamp/node correlation metadata, names, compensation +values, offer terms, and actor identities cannot be smuggled into the governance envelope +through a reference field. `reason_code` is likewise closed to the reviewed, value-free `selected_candidate_offer_review` code; arbitrary lower-snake-case text is rejected so the reason field cannot become a side channel for candidate, compensation, or offer-term values. From 7ab39cdb87e66a7fc8cd88f50b37977c24017e90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:30:56 -0700 Subject: [PATCH 35/49] docs: record UUIDv4 trust-reference decision --- docs/adr/0017-governed-offer-approval.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index d2b18b9a2..165c7ff49 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -15,7 +15,7 @@ Offer review is high-impact employment workflow. A governance envelope must not alternate decision authority, a salary-value cache, or a channel that lets generated/model material masquerade as an approved offer. Different opaque requester/approver references also do not prove that the authoritative actor boundary resolves them to different people, and -canonical UUID syntax does not prove that the referenced candidate, requisition, Job/Position, +canonical UUIDv4 syntax does not prove that the referenced candidate, requisition, Job/Position, selection decision, compensation package, or offer terms belong to the packet tenant. ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, @@ -29,13 +29,15 @@ or decide the legality of any offer. Orgmetra will expose `OfferApprovalPacket` as value-free review evidence only. The packet binds opaque references for the candidate profile, requisition, Job, optional -Position, selection decision, compensation package, and offer terms. Decision/package/terms -artifacts are independently SHA-256 bound. Before approval, the host must re-resolve **every -packet reference** within the exact `tenant_record_id` through its authoritative boundary and -reject approval if any reference belongs to another tenant or cannot be authoritatively -resolved. Identical requester/approver references are rejected as an early syntactic guard; -after tenant-scoped resolution, the host must prove their resolved actor identities are -distinct. Reference inequality alone is not separation-of-duties evidence. +Position, selection decision, compensation package, and offer terms. Every namespaced trust +reference requires a canonical non-sentinel UUIDv4 suffix; UUIDv1 and other UUID versions fail +closed so reference identity cannot carry UUIDv1 timestamp/node correlation metadata. Decision, +package, and terms artifacts are independently SHA-256 bound. Before approval, the host must +re-resolve **every packet reference** within the exact `tenant_record_id` through its +authoritative boundary and reject approval if any reference belongs to another tenant or cannot +be authoritatively resolved. Identical requester/approver references are rejected as an early +syntactic guard; after tenant-scoped resolution, the host must prove their resolved actor +identities are distinct. Reference inequality alone is not separation-of-duties evidence. The packet must not contain candidate PII, compensation values, assessment scores, or free-form model output. The `reason_code` field is not free-form metadata: it is closed to the From 741328ac449bdac139a0600636cb374da15e6660 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:31:07 -0700 Subject: [PATCH 36/49] docs: record offer reference privacy hardening --- packages/offer-approval/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/offer-approval/CHANGELOG.md b/packages/offer-approval/CHANGELOG.md index b617a302d..cd27ef5be 100644 --- a/packages/offer-approval/CHANGELOG.md +++ b/packages/offer-approval/CHANGELOG.md @@ -6,6 +6,8 @@ - Require separate requester and approver identities and exact human approval. - Bind selected-candidate, Job/optional Position, selection-decision, compensation-package, and offer-terms provenance without copying candidate or compensation values. +- Reject UUIDv1 and other non-v4 trust-reference suffixes so opaque packet references do not + carry UUIDv1 timestamp/node correlation metadata. - Close `reason_code` to the reviewed value-free `selected_candidate_offer_review` contract so arbitrary candidate, compensation, or offer-term text cannot enter canonical evidence. - Bind a bounded positive `evidence_version` into canonical JSON and SHA-256 correlation From 722286fd05494e3a6de8d56fc89718955549d696 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 19:31:25 -0700 Subject: [PATCH 37/49] docs: trace UUIDv4 offer reference regression --- docs/traceability/offer-approval.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md index b4b492e29..37b9c2de9 100644 --- a/docs/traceability/offer-approval.md +++ b/docs/traceability/offer-approval.md @@ -5,6 +5,7 @@ Status: **active PR / proposed capability**, not protected-main truth. | Buyer requirement | Executable evidence | Contract outcome | | --- | --- | --- | | Exact selected-candidate scope | `test_rejects_bad_opaque_references`; canonical JSON test | Candidate is correlated only by a bounded opaque `candidate_profile:` reference. | +| Opaque trust-reference privacy | `test_rejects_uuid1_trust_references_through_direct_and_replace` | Every namespaced packet reference requires a canonical non-sentinel UUIDv4 suffix; UUIDv1 timestamp/node correlation and other UUID versions fail closed through direct construction and replacement. | | Separate Job and Position | valid packet + optional-Position test | Job is mandatory; Position is separately named and optional rather than collapsed into Job. | | Reviewed selection evidence | digest/reference validation tests | Selection decision identity and SHA-256 evidence are required. | | Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | From 0a7e365b274f714c72be284d907d4c900cc8d939 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:48:38 -0700 Subject: [PATCH 38/49] test: reject correlating tenant UUIDv1 in offer approval --- .../tests/test_tenant_identity_privacy.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 packages/offer-approval/tests/test_tenant_identity_privacy.py diff --git a/packages/offer-approval/tests/test_tenant_identity_privacy.py b/packages/offer-approval/tests/test_tenant_identity_privacy.py new file mode 100644 index 000000000..9c9b49211 --- /dev/null +++ b/packages/offer-approval/tests/test_tenant_identity_privacy.py @@ -0,0 +1,44 @@ +"""Privacy regression for the public tenant identity in offer approval.""" +from dataclasses import replace +from datetime import datetime, timezone + +import pytest + +from orgmetra_offer_approval import build_offer_approval_packet + +UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + + +def _valid_kwargs() -> dict[str, object]: + """Return one valid offer-approval packet input mapping.""" + return { + "tenant_record_id": "11111111-1111-4111-8111-111111111111", + "offer_approval_reference": "offer_approval:10000000-0000-4000-8000-000000000001", + "candidate_profile_reference": "candidate_profile:10000000-0000-4000-8000-000000000002", + "requisition_reference": "requisition:10000000-0000-4000-8000-000000000003", + "job_profile_reference": "job_profile:10000000-0000-4000-8000-000000000004", + "position_record_reference": "position_record:10000000-0000-4000-8000-000000000005", + "selection_decision_reference": "selection_decision:10000000-0000-4000-8000-000000000006", + "selection_decision_digest": "a" * 64, + "compensation_package_reference": "compensation_package:10000000-0000-4000-8000-000000000007", + "compensation_package_digest": "b" * 64, + "offer_terms_reference": "offer_terms:10000000-0000-4000-8000-000000000008", + "offer_terms_digest": "c" * 64, + "requester_reference": "actor:10000000-0000-4000-8000-000000000009", + "approver_reference": "actor:10000000-0000-4000-8000-00000000000a", + "purpose_code": "offer_approval_review", + "reason_code": "selected_candidate_offer_review", + "generated_at": datetime(2026, 8, 19, 5, 10, tzinfo=timezone.utc), + } + + +def test_uuid1_tenant_identity_is_rejected_by_builder_and_replace() -> None: + """UUIDv1 timestamp/node metadata must not enter the public tenant identity.""" + kwargs = _valid_kwargs() + kwargs["tenant_record_id"] = UUID1_ID + with pytest.raises(ValueError, match="tenant_record_id"): + build_offer_approval_packet(**kwargs) + + packet = build_offer_approval_packet(**_valid_kwargs()) + with pytest.raises(ValueError, match="tenant_record_id"): + replace(packet, tenant_record_id=UUID1_ID) From d15f6bbbf44b13436985e1526edb6477df46a93b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:49:08 -0700 Subject: [PATCH 39/49] fix: require opaque UUIDv4 tenant identity in offer approval --- .../offer-approval/src/orgmetra_offer_approval/packet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index 37dd71310..d7dc4b91d 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -36,13 +36,13 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require canonical non-sentinel UUID text for a governance identity.""" + """Require canonical UUIDv4 text so a public governance identity stays opaque.""" try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): - raise ValueError(f"{field_name} must be a canonical operational UUID") + if str(parsed) != value or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUIDv4") def _validate_code(value: str, field_name: str) -> None: @@ -262,4 +262,4 @@ def build_offer_approval_packet( reason_code=reason_code, generated_at=generated_at, evidence_version=evidence_version, - ) + ) \ No newline at end of file From 658eec11c4b887d7264f01672e15150b79c4e694 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:49:31 -0700 Subject: [PATCH 40/49] docs: bind offer-approval tenant identity to UUIDv4 opacity --- packages/offer-approval/README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index 71ed0af9b..d7f2c3286 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -11,10 +11,11 @@ and approver references are rejected as an early syntactic guard. The envelope intentionally excludes candidate names, email addresses, demographic values, assessment scores, salary/benefit amounts, credentials, and free-form model output. `candidate_profile_reference` remains sensitive correlating metadata even though it is -opaque. Every namespaced reference uses a canonical, non-sentinel UUIDv4 suffix; UUIDv1 and -other UUID versions are rejected so timestamp/node correlation metadata, names, compensation -values, offer terms, and actor identities cannot be smuggled into the governance envelope -through a reference field. `reason_code` is likewise closed to the reviewed, value-free +opaque. The public `tenant_record_id` and every namespaced reference use canonical, +non-sentinel UUIDv4 identity; namespaced references additionally require their expected +namespace. UUIDv1 and other UUID versions are rejected so timestamp/node correlation metadata, +names, compensation values, offer terms, and actor identities cannot be smuggled into public +governance identity fields. `reason_code` is likewise closed to the reviewed, value-free `selected_candidate_offer_review` code; arbitrary lower-snake-case text is rejected so the reason field cannot become a side channel for candidate, compensation, or offer-term values. @@ -32,7 +33,8 @@ foreign tenant cannot be mixed into the approval envelope. It must specifically distinct; opaque-reference inequality alone is not separation-of-duties evidence. The host must then verify Job/Position scope, selected-candidate evidence, compensation-package provenance, and offer-terms provenance before recording accountable human approval through the -authoritative offer workflow and before communicating or executing the offer. +authoritative offer workflow and before communicating or executing the offer. UUIDv4 is only +an opacity constraint; it is not proof of tenant membership, authorization, or source truth. Canonical JSON and its SHA-256 digest support immutable audit correlation. They do not prove that the referenced evidence is true, that all references belong to the packet tenant, that @@ -64,4 +66,4 @@ packet = build_offer_approval_packet( reason_code="selected_candidate_offer_review", generated_at=datetime.now(timezone.utc), ) -``` +``` \ No newline at end of file From 2b4dfa8c37cdb07cf9e41841b832d05733d34907 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:49:51 -0700 Subject: [PATCH 41/49] docs: require UUIDv4 tenant opacity in offer approval ADR --- docs/adr/0017-governed-offer-approval.md | 36 +++++++++++++----------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index 165c7ff49..4f8654c22 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -16,7 +16,9 @@ alternate decision authority, a salary-value cache, or a channel that lets gener material masquerade as an approved offer. Different opaque requester/approver references also do not prove that the authoritative actor boundary resolves them to different people, and canonical UUIDv4 syntax does not prove that the referenced candidate, requisition, Job/Position, -selection decision, compensation package, or offer terms belong to the packet tenant. +selection decision, compensation package, or offer terms belong to the packet tenant. UUIDv1 +also carries timestamp/node-derived correlation metadata, so it is unsuitable for the public +tenant identity as well as fields represented as opaque references. ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, stakeholder management, and review. EEOC guidance on tests and selection procedures emphasizes @@ -28,16 +30,17 @@ or decide the legality of any offer. Orgmetra will expose `OfferApprovalPacket` as value-free review evidence only. -The packet binds opaque references for the candidate profile, requisition, Job, optional -Position, selection decision, compensation package, and offer terms. Every namespaced trust -reference requires a canonical non-sentinel UUIDv4 suffix; UUIDv1 and other UUID versions fail -closed so reference identity cannot carry UUIDv1 timestamp/node correlation metadata. Decision, -package, and terms artifacts are independently SHA-256 bound. Before approval, the host must -re-resolve **every packet reference** within the exact `tenant_record_id` through its -authoritative boundary and reject approval if any reference belongs to another tenant or cannot -be authoritatively resolved. Identical requester/approver references are rejected as an early -syntactic guard; after tenant-scoped resolution, the host must prove their resolved actor -identities are distinct. Reference inequality alone is not separation-of-duties evidence. +The packet requires canonical non-sentinel UUIDv4 for the public `tenant_record_id` and opaque +candidate profile, requisition, Job, optional Position, selection decision, compensation package, +offer terms, and accountable actor references; namespaced references additionally require their +expected prefix. UUIDv1 and other UUID versions fail closed so public governance identities +cannot carry timestamp/node-derived correlation metadata. Decision, package, and terms artifacts +are independently SHA-256 bound. Before approval, the host must re-resolve **every packet +reference** within the exact `tenant_record_id` through its authoritative boundary and reject +approval if any reference belongs to another tenant or cannot be authoritatively resolved. +Identical requester/approver references are rejected as an early syntactic guard; after +tenant-scoped resolution, the host must prove their resolved actor identities are distinct. +Reference inequality alone is not separation-of-duties evidence. The packet must not contain candidate PII, compensation values, assessment scores, or free-form model output. The `reason_code` field is not free-form metadata: it is closed to the @@ -67,10 +70,11 @@ communicate, send, execute, persist an offer, or prove authoritative reference/a A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, while Orgmetra keeps exact provenance references, evidence version, and human accountability. -Cross-tenant evidence mixing is fail-closed at the host approval boundary because every packet -reference must resolve in the exact tenant. Requester/approver separation is proven only after -tenant-scoped authoritative actor resolution. New offer-review reason categories require an -explicit contract change and regression evidence rather than accepting arbitrary caller text. +UUIDv1/non-v4 public tenant/reference identities fail closed before serialization. Cross-tenant +evidence mixing is fail-closed at the host approval boundary because every packet reference +must resolve in the exact tenant. Requester/approver separation is proven only after tenant- +scoped authoritative actor resolution. New offer-review reason categories require an explicit +contract change and regression evidence rather than accepting arbitrary caller text. Downstream offer persistence/execution must independently enforce authorization, tenant-scoped source-evidence resolution, idempotency where applicable, and immutable audit/outbox evidence. @@ -78,4 +82,4 @@ This ADR remains proposed active-PR truth until integrated into protected `devel ## References -See `docs/doctoring/offer-approval-references.md`. +See `docs/doctoring/offer-approval-references.md`. \ No newline at end of file From dd3b4820143c78587f9e1346cf2070d291b3ae62 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:50:06 -0700 Subject: [PATCH 42/49] docs: trace UUIDv4 tenant opacity in offer approval --- docs/traceability/offer-approval.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md index 37b9c2de9..34c7ae550 100644 --- a/docs/traceability/offer-approval.md +++ b/docs/traceability/offer-approval.md @@ -5,7 +5,7 @@ Status: **active PR / proposed capability**, not protected-main truth. | Buyer requirement | Executable evidence | Contract outcome | | --- | --- | --- | | Exact selected-candidate scope | `test_rejects_bad_opaque_references`; canonical JSON test | Candidate is correlated only by a bounded opaque `candidate_profile:` reference. | -| Opaque trust-reference privacy | `test_rejects_uuid1_trust_references_through_direct_and_replace` | Every namespaced packet reference requires a canonical non-sentinel UUIDv4 suffix; UUIDv1 timestamp/node correlation and other UUID versions fail closed through direct construction and replacement. | +| Opaque public identity/reference privacy | `test_tenant_identity_privacy.py`; `test_rejects_uuid1_trust_references_through_direct_and_replace` | `tenant_record_id` and every namespaced packet reference require canonical non-sentinel UUIDv4 identity; UUIDv1 timestamp/node correlation and other UUID versions fail closed through construction and replacement paths. | | Separate Job and Position | valid packet + optional-Position test | Job is mandatory; Position is separately named and optional rather than collapsed into Job. | | Reviewed selection evidence | digest/reference validation tests | Selection decision identity and SHA-256 evidence are required. | | Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | @@ -16,8 +16,10 @@ Status: **active PR / proposed capability**, not protected-main truth. | Deterministic audit correlation | canonical JSON, fractional-second, timezone, evidence-version, SHA-256 tests | Canonical evidence is precision-preserving, versioned, and deterministic. | | Public API readability | module/class/function docstrings | Beginner-readable contract boundary is documented in source and package README. | +UUIDv4 is an opacity constraint, not tenant authority. Before approval, every packet reference must still resolve authoritatively inside the exact `tenant_record_id`; requester/approver identity separation must be proven after that resolution. + The SHA-256 packet digest proves only the exact canonical envelope bytes. It does not prove that referenced evidence is substantively correct, that requester/approver resolve to different identities, that compensation is lawful/fair, that a human approved the offer, or that an offer was delivered. Authoritative actor and source-evidence resolution remain outside this evidence -packet and are required pre-approval host checks. +packet and are required pre-approval host checks. \ No newline at end of file From 2975c0c606fa009db35787de5d8104f16db2a3f5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 00:50:19 -0700 Subject: [PATCH 43/49] docs: record offer-approval tenant UUIDv4 hardening --- packages/offer-approval/CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/offer-approval/CHANGELOG.md b/packages/offer-approval/CHANGELOG.md index cd27ef5be..a62e3b561 100644 --- a/packages/offer-approval/CHANGELOG.md +++ b/packages/offer-approval/CHANGELOG.md @@ -6,10 +6,9 @@ - Require separate requester and approver identities and exact human approval. - Bind selected-candidate, Job/optional Position, selection-decision, compensation-package, and offer-terms provenance without copying candidate or compensation values. -- Reject UUIDv1 and other non-v4 trust-reference suffixes so opaque packet references do not - carry UUIDv1 timestamp/node correlation metadata. +- Require canonical non-sentinel UUIDv4 for the public `tenant_record_id` and every trust-reference suffix so public packet identities cannot carry UUIDv1 timestamp/node correlation metadata or other non-v4 identity forms. - Close `reason_code` to the reviewed value-free `selected_candidate_offer_review` contract so arbitrary candidate, compensation, or offer-term text cannot enter canonical evidence. - Bind a bounded positive `evidence_version` into canonical JSON and SHA-256 correlation evidence so high-impact offer-review evidence versions are explicit and fail closed. -- Keep every packet `requires_human_approval` and `not_authorized_to_send`. +- Keep every packet `requires_human_approval` and `not_authorized_to_send`. \ No newline at end of file From c48179290756c6e7fe3a4b3e9c732c97de016614 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:22:52 -0700 Subject: [PATCH 44/49] test: require offer approval to accept core tenant UUIDv7 --- .../tests/test_tenant_identity_privacy.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/packages/offer-approval/tests/test_tenant_identity_privacy.py b/packages/offer-approval/tests/test_tenant_identity_privacy.py index 9c9b49211..05a916973 100644 --- a/packages/offer-approval/tests/test_tenant_identity_privacy.py +++ b/packages/offer-approval/tests/test_tenant_identity_privacy.py @@ -1,12 +1,10 @@ -"""Privacy regression for the public tenant identity in offer approval.""" +"""Privacy and interoperability regression for offer-approval tenant identity.""" from dataclasses import replace from datetime import datetime, timezone -import pytest - from orgmetra_offer_approval import build_offer_approval_packet -UUID1_ID = "6ba7b810-9dad-11d1-80b4-00c04fd430c8" +UUID7_TENANT = "10000000-0000-7000-8000-000000000001" def _valid_kwargs() -> dict[str, object]: @@ -32,13 +30,13 @@ def _valid_kwargs() -> dict[str, object]: } -def test_uuid1_tenant_identity_is_rejected_by_builder_and_replace() -> None: - """UUIDv1 timestamp/node metadata must not enter the public tenant identity.""" +def test_authoritative_uuid7_tenant_identity_is_accepted_by_builder_and_replace() -> None: + """Accept tenant UUIDs already valid at the authoritative Orgmetra core boundary.""" kwargs = _valid_kwargs() - kwargs["tenant_record_id"] = UUID1_ID - with pytest.raises(ValueError, match="tenant_record_id"): - build_offer_approval_packet(**kwargs) + kwargs["tenant_record_id"] = UUID7_TENANT + + packet = build_offer_approval_packet(**kwargs) + replaced = replace(build_offer_approval_packet(**_valid_kwargs()), tenant_record_id=UUID7_TENANT) - packet = build_offer_approval_packet(**_valid_kwargs()) - with pytest.raises(ValueError, match="tenant_record_id"): - replace(packet, tenant_record_id=UUID1_ID) + assert packet.tenant_record_id == UUID7_TENANT + assert replaced.tenant_record_id == UUID7_TENANT From 01cc7bc7f8f3ba7a948f6488a1eea393eb73bede Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:23:26 -0700 Subject: [PATCH 45/49] fix: honor authoritative tenant UUID contract in offer approval --- .../offer-approval/src/orgmetra_offer_approval/packet.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/offer-approval/src/orgmetra_offer_approval/packet.py b/packages/offer-approval/src/orgmetra_offer_approval/packet.py index d7dc4b91d..3a7363c62 100644 --- a/packages/offer-approval/src/orgmetra_offer_approval/packet.py +++ b/packages/offer-approval/src/orgmetra_offer_approval/packet.py @@ -36,13 +36,13 @@ def _validate_operational_uuid(value: str, field_name: str) -> None: - """Require canonical UUIDv4 text so a public governance identity stays opaque.""" + """Require canonical non-sentinel UUID text owned by the authoritative HRIS.""" try: parsed = UUID(value) except (ValueError, AttributeError, TypeError) as exc: raise ValueError(f"{field_name} must be canonical UUID text") from exc - if str(parsed) != value or parsed.version != 4 or parsed.int in (0, (1 << 128) - 1): - raise ValueError(f"{field_name} must be a canonical operational UUIDv4") + if str(parsed) != value or parsed.int in (0, (1 << 128) - 1): + raise ValueError(f"{field_name} must be a canonical operational UUID") def _validate_code(value: str, field_name: str) -> None: @@ -262,4 +262,4 @@ def build_offer_approval_packet( reason_code=reason_code, generated_at=generated_at, evidence_version=evidence_version, - ) \ No newline at end of file + ) From 7bd84ab89bc02467e5f56234149b469c4d90dec3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:23:45 -0700 Subject: [PATCH 46/49] docs: align offer tenant identity with Orgmetra core --- packages/offer-approval/README.md | 43 +++++-------------------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/packages/offer-approval/README.md b/packages/offer-approval/README.md index d7f2c3286..9fb659a14 100644 --- a/packages/offer-approval/README.md +++ b/packages/offer-approval/README.md @@ -1,45 +1,16 @@ # Orgmetra governed offer approval -This package creates a **value-free pre-send offer approval packet**. It is a governance -envelope, not an offer engine and not an employment decision. +This package creates a **value-free pre-send offer approval packet**. It is a governance envelope, not an offer engine and not an employment decision. -The packet binds one selected candidate to the exact requisition and authoritative Job, -an optional exact Position, the reviewed selection-decision digest, compensation-package -provenance, offer-terms provenance, and two accountable actor references. Identical requester -and approver references are rejected as an early syntactic guard. +The packet binds one selected candidate to the exact requisition and authoritative Job, an optional exact Position, the reviewed selection-decision digest, compensation-package provenance, offer-terms provenance, and two accountable actor references. Identical requester and approver references are rejected as an early syntactic guard. -The envelope intentionally excludes candidate names, email addresses, demographic values, -assessment scores, salary/benefit amounts, credentials, and free-form model output. -`candidate_profile_reference` remains sensitive correlating metadata even though it is -opaque. The public `tenant_record_id` and every namespaced reference use canonical, -non-sentinel UUIDv4 identity; namespaced references additionally require their expected -namespace. UUIDv1 and other UUID versions are rejected so timestamp/node correlation metadata, -names, compensation values, offer terms, and actor identities cannot be smuggled into public -governance identity fields. `reason_code` is likewise closed to the reviewed, value-free -`selected_candidate_offer_review` code; arbitrary lower-snake-case text is rejected so the -reason field cannot become a side channel for candidate, compensation, or offer-term values. +The envelope intentionally excludes candidate names, email addresses, demographic values, assessment scores, salary/benefit amounts, credentials, and free-form model output. `candidate_profile_reference` remains sensitive correlating metadata even though it is opaque. `tenant_record_id` follows Orgmetra's authoritative canonical non-sentinel operational UUID contract rather than imposing a second UUID-version rule in this leaf package. Packet-owned namespaced references remain canonical non-sentinel UUIDv4 values and require their expected namespace. UUIDv1 and other non-v4 reference suffixes are rejected so timestamp/node correlation metadata, names, compensation values, offer terms, and actor identities cannot be smuggled into packet-owned governance references. `reason_code` is likewise closed to the reviewed, value-free `selected_candidate_offer_review` code; arbitrary lower-snake-case text is rejected so the reason field cannot become a side channel for candidate, compensation, or offer-term values. -Every packet also carries a bounded positive integer `evidence_version` (default `1`). It is -serialized into canonical JSON, so changing the governed evidence version changes the packet -SHA-256 digest. Zero, negative, boolean, textual, and values above `2147483647` fail closed. -The field versions this immutable pre-send evidence envelope; it is not approval, delivery, -or proof that referenced source versions were authoritatively resolved. +Every packet also carries a bounded positive integer `evidence_version` (default `1`). It is serialized into canonical JSON, so changing the governed evidence version changes the packet SHA-256 digest. Zero, negative, boolean, textual, and values above `2147483647` fail closed. The field versions this immutable pre-send evidence envelope; it is not approval, delivery, or proof that referenced source versions were authoritatively resolved. -A valid packet always remains `requires_human_approval` and -`not_authorized_to_send`. Before approval, the host must re-resolve **every packet reference** -within the exact `tenant_record_id` through its authoritative boundary so valid UUIDs from a -foreign tenant cannot be mixed into the approval envelope. It must specifically re-resolve -`requester_reference` and `approver_reference` and prove their resolved actor identities are -distinct; opaque-reference inequality alone is not separation-of-duties evidence. The host -must then verify Job/Position scope, selected-candidate evidence, compensation-package -provenance, and offer-terms provenance before recording accountable human approval through the -authoritative offer workflow and before communicating or executing the offer. UUIDv4 is only -an opacity constraint; it is not proof of tenant membership, authorization, or source truth. +A valid packet always remains `requires_human_approval` and `not_authorized_to_send`. Before approval, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary so valid references from a foreign tenant cannot be mixed into the approval envelope. It must specifically re-resolve `requester_reference` and `approver_reference` and prove their resolved actor identities are distinct; opaque-reference inequality alone is not separation-of-duties evidence. The host must then verify Job/Position scope, selected-candidate evidence, compensation-package provenance, and offer-terms provenance before recording accountable human approval through the authoritative offer workflow and before communicating or executing the offer. UUIDv4 is only an opacity constraint for packet-owned references; tenant UUID generation/privacy policy remains owned by the authoritative HRIS boundary. -Canonical JSON and its SHA-256 digest support immutable audit correlation. They do not prove -that the referenced evidence is true, that all references belong to the packet tenant, that -actor identities are distinct, that compensation is lawful or fair, that an offer was approved, -or that an offer was communicated. +Canonical JSON and its SHA-256 digest support immutable audit correlation. They do not prove that the referenced evidence is true, that all references belong to the packet tenant, that actor identities are distinct, that compensation is lawful or fair, that an offer was approved, or that an offer was communicated. ## Example @@ -66,4 +37,4 @@ packet = build_offer_approval_packet( reason_code="selected_candidate_offer_review", generated_at=datetime.now(timezone.utc), ) -``` \ No newline at end of file +``` From 84195271ea8f75bd71e61021a31b0a33d89aa310 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:24:08 -0700 Subject: [PATCH 47/49] docs: separate offer tenant and packet UUID ownership --- docs/adr/0017-governed-offer-approval.md | 82 +++++------------------- 1 file changed, 17 insertions(+), 65 deletions(-) diff --git a/docs/adr/0017-governed-offer-approval.md b/docs/adr/0017-governed-offer-approval.md index 4f8654c22..c662a0429 100644 --- a/docs/adr/0017-governed-offer-approval.md +++ b/docs/adr/0017-governed-offer-approval.md @@ -6,80 +6,32 @@ ## Context -Protected `develop` can govern candidate, requisition, selection, and employment evidence, -but it does not yet expose a bounded pre-send contract proving that a proposed offer is tied -to the selected candidate, authoritative Job/optional Position, reviewed selection decision, -compensation-package provenance, offer-terms provenance, and accountable human approval. - -Offer review is high-impact employment workflow. A governance envelope must not become an -alternate decision authority, a salary-value cache, or a channel that lets generated/model -material masquerade as an approved offer. Different opaque requester/approver references also -do not prove that the authoritative actor boundary resolves them to different people, and -canonical UUIDv4 syntax does not prove that the referenced candidate, requisition, Job/Position, -selection decision, compensation package, or offer terms belong to the packet tenant. UUIDv1 -also carries timestamp/node-derived correlation metadata, so it is unsuitable for the public -tenant identity as well as fields represented as opaque references. - -ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, -stakeholder management, and review. EEOC guidance on tests and selection procedures emphasizes -job-related use and employer responsibility for selection procedures. Those sources support a -conservative evidence-and-human-review boundary; they do not by themselves certify this package -or decide the legality of any offer. +Protected `develop` can govern candidate, requisition, selection, and employment evidence, but it does not yet expose a bounded pre-send contract proving that a proposed offer is tied to the selected candidate, authoritative Job/optional Position, reviewed selection decision, compensation-package provenance, offer-terms provenance, and accountable human approval. + +Offer review is high-impact employment workflow. A governance envelope must not become an alternate decision authority, a salary-value cache, or a channel that lets generated/model material masquerade as an approved offer. Different opaque requester/approver references also do not prove that the authoritative actor boundary resolves them to different people, and UUID syntax does not prove that the referenced candidate, requisition, Job/Position, selection decision, compensation package, or offer terms belong to the packet tenant. Packet-owned UUIDv1 references also carry timestamp/node-derived correlation metadata. The authoritative tenant identifier is different: it is issued by Orgmetra core, so this leaf package must accept the canonical non-sentinel operational UUID contract owned by that boundary rather than silently imposing a second version policy. + +ISO 30405:2023 provides current recruitment guidance across planning, assessment, employment, stakeholder management, and review. EEOC guidance on tests and selection procedures emphasizes job-related use and employer responsibility for selection procedures. Those sources support a conservative evidence-and-human-review boundary; they do not by themselves certify this package or decide the legality of any offer. ## Decision Orgmetra will expose `OfferApprovalPacket` as value-free review evidence only. -The packet requires canonical non-sentinel UUIDv4 for the public `tenant_record_id` and opaque -candidate profile, requisition, Job, optional Position, selection decision, compensation package, -offer terms, and accountable actor references; namespaced references additionally require their -expected prefix. UUIDv1 and other UUID versions fail closed so public governance identities -cannot carry timestamp/node-derived correlation metadata. Decision, package, and terms artifacts -are independently SHA-256 bound. Before approval, the host must re-resolve **every packet -reference** within the exact `tenant_record_id` through its authoritative boundary and reject -approval if any reference belongs to another tenant or cannot be authoritatively resolved. -Identical requester/approver references are rejected as an early syntactic guard; after -tenant-scoped resolution, the host must prove their resolved actor identities are distinct. -Reference inequality alone is not separation-of-duties evidence. - -The packet must not contain candidate PII, compensation values, assessment scores, or -free-form model output. The `reason_code` field is not free-form metadata: it is closed to the -reviewed value-free `selected_candidate_offer_review` code so syntactically valid text cannot -smuggle candidate, compensation, or offer-term values into canonical evidence. Direct -construction and `dataclasses.replace(...)` revalidate all trust-bearing invariants. - -Every packet is fixed to: - -- purpose `offer_approval_review`; -- reviewed reason `selected_candidate_offer_review`; -- bounded positive integer `evidence_version` (default `1`), included in canonical JSON/SHA-256; -- `human_confirmation_required=True`; -- decision authority `human_approval_only`; -- review state `requires_human_approval`; -- delivery state `not_authorized_to_send`. - -`evidence_version` accepts only real integers from `1` through `2147483647`; booleans, text, -zero, negative values, and overflow values fail closed. It versions the immutable pre-send -evidence envelope and does not itself prove source-version resolution, approval, or delivery. - -Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does not approve, -communicate, send, execute, persist an offer, or prove authoritative reference/actor identity. +`tenant_record_id` must be canonical and non-sentinel under Orgmetra's authoritative operational UUID contract. Tenant UUID generation/version/privacy policy remains owned by the core HRIS boundary. Packet-owned opaque candidate profile, requisition, Job, optional Position, selection decision, compensation package, offer terms, and accountable actor references separately require canonical non-sentinel UUIDv4 plus their expected namespace. UUIDv1 and other non-v4 suffixes fail closed for those packet-owned references. Decision, package, and terms artifacts are independently SHA-256 bound. Before approval, the host must re-resolve **every packet reference** within the exact `tenant_record_id` through its authoritative boundary and reject approval if any reference belongs to another tenant or cannot be authoritatively resolved. Identical requester/approver references are rejected as an early syntactic guard; after tenant-scoped resolution, the host must prove their resolved actor identities are distinct. Reference inequality alone is not separation-of-duties evidence. + +The packet must not contain candidate PII, compensation values, assessment scores, or free-form model output. The `reason_code` field is closed to the reviewed value-free `selected_candidate_offer_review` code. Direct construction and `dataclasses.replace(...)` revalidate all trust-bearing invariants. + +Every packet is fixed to purpose `offer_approval_review`, reviewed reason `selected_candidate_offer_review`, bounded positive integer `evidence_version` (default `1`) included in canonical JSON/SHA-256, `human_confirmation_required=True`, decision authority `human_approval_only`, review state `requires_human_approval`, and delivery state `not_authorized_to_send`. + +`evidence_version` accepts only real integers from `1` through `2147483647`; booleans, text, zero, negative values, and overflow values fail closed. It versions the immutable pre-send evidence envelope and does not itself prove source-version resolution, approval, or delivery. + +Canonical JSON and SHA-256 are audit-correlation evidence only. The packet does not approve, communicate, send, execute, persist an offer, or prove authoritative reference/actor identity. ## Consequences -A buyer can review one deterministic, PII-minimized envelope before an offer moves to the -authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, -while Orgmetra keeps exact provenance references, evidence version, and human accountability. -UUIDv1/non-v4 public tenant/reference identities fail closed before serialization. Cross-tenant -evidence mixing is fail-closed at the host approval boundary because every packet reference -must resolve in the exact tenant. Requester/approver separation is proven only after tenant- -scoped authoritative actor resolution. New offer-review reason categories require an explicit -contract change and regression evidence rather than accepting arbitrary caller text. +A buyer can review one deterministic, PII-minimized envelope before an offer moves to the authoritative offer workflow. Compensation values stay in their purpose-bound owner boundary, while Orgmetra keeps exact provenance references, evidence version, and human accountability. Packet-owned UUIDv1/non-v4 references fail closed before serialization without making this leaf package incompatible with authoritative Orgmetra tenant UUIDs. Cross-tenant evidence mixing is fail-closed at the host approval boundary because every packet reference must resolve in the exact tenant. Requester/approver separation is proven only after tenant-scoped authoritative actor resolution. New offer-review reason categories require an explicit contract change and regression evidence rather than accepting arbitrary caller text. -Downstream offer persistence/execution must independently enforce authorization, tenant-scoped -source-evidence resolution, idempotency where applicable, and immutable audit/outbox evidence. -This ADR remains proposed active-PR truth until integrated into protected `develop`. +Downstream offer persistence/execution must independently enforce authorization, tenant-scoped source-evidence resolution, idempotency where applicable, and immutable audit/outbox evidence. This ADR remains proposed active-PR truth until integrated into protected `develop`. ## References -See `docs/doctoring/offer-approval-references.md`. \ No newline at end of file +See `docs/doctoring/offer-approval-references.md`. From 9810102685254ef3cb21b027586fcdad6aeee9ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:24:21 -0700 Subject: [PATCH 48/49] docs: trace offer tenant UUID interoperability --- docs/traceability/offer-approval.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/traceability/offer-approval.md b/docs/traceability/offer-approval.md index 34c7ae550..5104cfb8d 100644 --- a/docs/traceability/offer-approval.md +++ b/docs/traceability/offer-approval.md @@ -5,7 +5,7 @@ Status: **active PR / proposed capability**, not protected-main truth. | Buyer requirement | Executable evidence | Contract outcome | | --- | --- | --- | | Exact selected-candidate scope | `test_rejects_bad_opaque_references`; canonical JSON test | Candidate is correlated only by a bounded opaque `candidate_profile:` reference. | -| Opaque public identity/reference privacy | `test_tenant_identity_privacy.py`; `test_rejects_uuid1_trust_references_through_direct_and_replace` | `tenant_record_id` and every namespaced packet reference require canonical non-sentinel UUIDv4 identity; UUIDv1 timestamp/node correlation and other UUID versions fail closed through construction and replacement paths. | +| Authoritative tenant interoperability and packet-reference privacy | `test_tenant_identity_privacy.py`; `test_rejects_uuid1_trust_references_through_direct_and_replace` | `tenant_record_id` follows the canonical non-sentinel Orgmetra core operational-UUID contract; packet-owned namespaced references require canonical non-sentinel UUIDv4 and reject UUIDv1/non-v4 suffixes through construction and replacement paths. | | Separate Job and Position | valid packet + optional-Position test | Job is mandatory; Position is separately named and optional rather than collapsed into Job. | | Reviewed selection evidence | digest/reference validation tests | Selection decision identity and SHA-256 evidence are required. | | Compensation/terms provenance without value duplication | value-free canonical JSON test; digest/reference validation tests | Package and terms are exact reference+digest pairs; salary/benefit values are absent. | @@ -16,10 +16,6 @@ Status: **active PR / proposed capability**, not protected-main truth. | Deterministic audit correlation | canonical JSON, fractional-second, timezone, evidence-version, SHA-256 tests | Canonical evidence is precision-preserving, versioned, and deterministic. | | Public API readability | module/class/function docstrings | Beginner-readable contract boundary is documented in source and package README. | -UUIDv4 is an opacity constraint, not tenant authority. Before approval, every packet reference must still resolve authoritatively inside the exact `tenant_record_id`; requester/approver identity separation must be proven after that resolution. +UUIDv4 is an opacity constraint for packet-owned trust references, not tenant authority. Tenant UUID generation/version/privacy policy remains owned by the authoritative HRIS boundary. Before approval, every packet reference must still resolve authoritatively inside the exact `tenant_record_id`; requester/approver identity separation must be proven after that resolution. -The SHA-256 packet digest proves only the exact canonical envelope bytes. It does not prove that -referenced evidence is substantively correct, that requester/approver resolve to different -identities, that compensation is lawful/fair, that a human approved the offer, or that an offer -was delivered. Authoritative actor and source-evidence resolution remain outside this evidence -packet and are required pre-approval host checks. \ No newline at end of file +The SHA-256 packet digest proves only the exact canonical envelope bytes. It does not prove that referenced evidence is substantively correct, that requester/approver resolve to different identities, that compensation is lawful/fair, that a human approved the offer, or that an offer was delivered. Authoritative actor and source-evidence resolution remain outside this evidence packet and are required pre-approval host checks. From ed0f67a283da616f6ed703ec9999ac82b434868f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 06:24:32 -0700 Subject: [PATCH 49/49] docs: record offer tenant identity interoperability repair --- packages/offer-approval/CHANGELOG.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/offer-approval/CHANGELOG.md b/packages/offer-approval/CHANGELOG.md index a62e3b561..626500f50 100644 --- a/packages/offer-approval/CHANGELOG.md +++ b/packages/offer-approval/CHANGELOG.md @@ -4,11 +4,8 @@ - Add a governed, value-free pre-send offer approval packet. - Require separate requester and approver identities and exact human approval. -- Bind selected-candidate, Job/optional Position, selection-decision, compensation-package, - and offer-terms provenance without copying candidate or compensation values. -- Require canonical non-sentinel UUIDv4 for the public `tenant_record_id` and every trust-reference suffix so public packet identities cannot carry UUIDv1 timestamp/node correlation metadata or other non-v4 identity forms. -- Close `reason_code` to the reviewed value-free `selected_candidate_offer_review` contract so - arbitrary candidate, compensation, or offer-term text cannot enter canonical evidence. -- Bind a bounded positive `evidence_version` into canonical JSON and SHA-256 correlation - evidence so high-impact offer-review evidence versions are explicit and fail closed. -- Keep every packet `requires_human_approval` and `not_authorized_to_send`. \ No newline at end of file +- Bind selected-candidate, Job/optional Position, selection-decision, compensation-package, and offer-terms provenance without copying candidate or compensation values. +- Follow Orgmetra's authoritative canonical non-sentinel operational UUID contract for `tenant_record_id`, while packet-owned trust-reference suffixes remain canonical non-sentinel UUIDv4 and reject UUIDv1/non-v4 identity forms. +- Close `reason_code` to the reviewed value-free `selected_candidate_offer_review` contract so arbitrary candidate, compensation, or offer-term text cannot enter canonical evidence. +- Bind a bounded positive `evidence_version` into canonical JSON and SHA-256 correlation evidence so high-impact offer-review evidence versions are explicit and fail closed. +- Keep every packet `requires_human_approval` and `not_authorized_to_send`.