From d9775c3d233e8496e66581f890c918df7f919b38 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:07:57 -0700 Subject: [PATCH 01/21] test(talent): define offer-to-hire close contract --- .../tests/test_offer_to_hire_close.py | 290 ++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 services/people-api/tests/test_offer_to_hire_close.py diff --git a/services/people-api/tests/test_offer_to_hire_close.py b/services/people-api/tests/test_offer_to_hire_close.py new file mode 100644 index 000000000..3185528a3 --- /dev/null +++ b/services/people-api/tests/test_offer_to_hire_close.py @@ -0,0 +1,290 @@ +"""Executable contracts for closing accepted offers through authoritative hire materialization.""" + +from __future__ import annotations + +from dataclasses import replace +from datetime import date, datetime, timezone +import unittest +from uuid import UUID + +from orgmetra_candidate_offer_response import build_candidate_offer_response +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import HireAcceptanceCommand, HireAcceptanceResult +from orgmetra_people_api.offer_close import ( + CandidateOfferHireAuthority, + CandidateOfferHireVerification, + OfferToHireIntegrityError, + close_accepted_offer_to_hire, +) + +TENANT = UUID("0198a412-7000-7000-8000-000000000001") +CANDIDATE = UUID("0198a412-7000-7000-8000-000000000010") +DECISION = UUID("0198a412-7000-7000-8000-000000000011") +PERSON = UUID("0198a412-7000-7000-8000-000000000020") +PERSON_NAME = UUID("0198a412-7000-7000-8000-000000000021") +EMPLOYMENT = UUID("0198a412-7000-7000-8000-000000000030") +EMPLOYMENT_VERSION = UUID("0198a412-7000-7000-8000-000000000031") +CONVERSION = UUID("0198a412-7000-7000-8000-000000000040") +AUDIT_EVENT = UUID("0198a412-7000-7000-8000-000000000050") +OUTBOX_DELIVERY = UUID("0198a412-7000-7000-8000-000000000051") +CANDIDATE_PROFILE_REFERENCE = "candidate_profile:6ba7b810-9dad-41d1-80b4-00c04fd430c8" +RESPONSE_REFERENCE = "candidate_offer_response:6ba7b811-9dad-41d1-80b4-00c04fd430c8" +OFFER_APPROVAL_REFERENCE = "offer_approval:6ba7b812-9dad-41d1-80b4-00c04fd430c8" +OFFER_TERMS_REFERENCE = "offer_terms:6ba7b813-9dad-41d1-80b4-00c04fd430c8" +IDENTITY_RESOLUTION_REFERENCE = "identity_resolution:6ba7b814-9dad-41d1-80b4-00c04fd430c8" +AUTHORITY_EVIDENCE_REFERENCE = "offer_hire_verification:6ba7b815-9dad-41d1-80b4-00c04fd430c8" +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +DIGEST_D = "d" * 64 +RESPONDED_AT = datetime(2026, 8, 24, 7, 0, tzinfo=timezone.utc) +RECORDED_AT = datetime(2026, 8, 24, 7, 1, tzinfo=timezone.utc) + + +def response(*, response_code: str = "offer_accepted"): + """Build one value-minimized candidate offer response.""" + return build_candidate_offer_response( + tenant_record_id=str(TENANT), + offer_response_reference=RESPONSE_REFERENCE, + candidate_profile_reference=CANDIDATE_PROFILE_REFERENCE, + offer_approval_reference=OFFER_APPROVAL_REFERENCE, + offer_approval_digest=DIGEST_A, + offer_terms_reference=OFFER_TERMS_REFERENCE, + offer_terms_digest=DIGEST_B, + candidate_actor_reference="candidate:subject-17", + identity_resolution_reference=IDENTITY_RESOLUTION_REFERENCE, + identity_resolution_digest=DIGEST_C, + response_code=response_code, + responded_at=RESPONDED_AT, + recorded_at=RECORDED_AT, + ) + + +def command(**overrides: object) -> HireAcceptanceCommand: + """Build one deterministic confirmed-hire command.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "candidate_profile_id": CANDIDATE, + "selection_decision_id": DECISION, + "person_record_id": PERSON, + "person_name_record_id": PERSON_NAME, + "employment_record_id": EMPLOYMENT, + "employment_record_version_id": EMPLOYMENT_VERSION, + "candidate_worker_conversion_record_id": CONVERSION, + "audit_event_record_id": AUDIT_EVENT, + "outbox_delivery_record_id": OUTBOX_DELIVERY, + "effective_from": date(2026, 8, 25), + "display_name": "Ada Lovelace", + "idempotency_key": "offer-close-idempotency-17", + "employment_status_code": "active", + } + values.update(overrides) + return HireAcceptanceCommand(**values) # type: ignore[arg-type] + + +PRINCIPAL = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-17", + granted_scope_codes=frozenset({"orgmetra.people.materialize_worker"}), +) +POLICY = PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="people-hire-v1", + resource_kind="selection_decision", + purpose_code="candidate_hire", + operation_code="materialize_worker", + required_scope_code="orgmetra.people.materialize_worker", + permitted_fields=frozenset({"candidate_worker_conversion"}), +) + + +class RecordingHirePort: + """Capture authoritative hire calls without persisting HR data.""" + + def __init__(self) -> None: + self.calls: list[HireAcceptanceCommand] = [] + + def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: + """Record one already-authorized hire command.""" + del authorization + self.calls.append(command) + return HireAcceptanceResult( + person_record_id=command.person_record_id, + employment_record_id=command.employment_record_id, + candidate_worker_conversion_record_id=command.candidate_worker_conversion_record_id, + ) + + +class RecordingAuthority: + """Resolve candidate-response evidence to exact authoritative hire scope.""" + + def __init__(self) -> None: + self.calls: list[dict[str, object]] = [] + self.mutate_response = None + self.override: dict[str, object] = {} + + def verify_offer_acceptance(self, **scope: object) -> CandidateOfferHireVerification: + """Return exact-scope authority evidence for the reviewed response.""" + self.calls.append(scope) + if self.mutate_response is not None: + self.mutate_response() + values: dict[str, object] = { + "tenant_record_id": TENANT, + "candidate_profile_id": CANDIDATE, + "selection_decision_id": DECISION, + "offer_response_digest": scope["offer_response_digest"], + "offer_approval_digest": DIGEST_A, + "offer_terms_digest": DIGEST_B, + "candidate_actor_reference": "candidate:subject-17", + "authority_evidence_reference": AUTHORITY_EVIDENCE_REFERENCE, + "authority_evidence_digest": DIGEST_D, + } + values.update(self.override) + return CandidateOfferHireVerification(**values) # type: ignore[arg-type] + + +class OfferToHireCloseTests(unittest.TestCase): + """Prove candidate response is necessary evidence but never hire authority itself.""" + + def test_declined_offer_never_reaches_authority_or_hire_port(self) -> None: + """A candidate decline must stop before authoritative hire work.""" + authority = RecordingAuthority() + port = RecordingHirePort() + + with self.assertRaisesRegex(OfferToHireIntegrityError, "accepted"): + close_accepted_offer_to_hire( + response=response(response_code="offer_declined"), + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=port, + ) + + self.assertEqual(authority.calls, []) + self.assertEqual(port.calls, []) + + def test_accepted_offer_requires_authoritative_mapping_before_hire(self) -> None: + """A valid response reaches the existing confirmed-hire path only after exact-scope verification.""" + authority = RecordingAuthority() + port = RecordingHirePort() + packet = response() + + result = close_accepted_offer_to_hire( + response=packet, + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=port, + ) + + self.assertIsInstance(authority, CandidateOfferHireAuthority) + self.assertEqual(result.employment_record_id, EMPLOYMENT) + self.assertEqual(port.calls, [command()]) + self.assertEqual(len(authority.calls), 1) + scope = authority.calls[0] + self.assertEqual(scope["tenant_record_id"], str(TENANT)) + self.assertEqual(scope["candidate_profile_reference"], CANDIDATE_PROFILE_REFERENCE) + self.assertEqual(scope["selection_decision_id"], DECISION) + self.assertEqual(scope["offer_response_digest"], packet.sha256_digest()) + self.assertEqual(scope["offer_approval_digest"], DIGEST_A) + self.assertEqual(scope["offer_terms_digest"], DIGEST_B) + + def test_authority_scope_mismatch_blocks_hire(self) -> None: + """A response-to-selection mapping cannot be widened after authority verification.""" + authority = RecordingAuthority() + authority.override["selection_decision_id"] = UUID("0198a412-7000-7000-8000-000000000099") + port = RecordingHirePort() + + with self.assertRaisesRegex(OfferToHireIntegrityError, "selection decision"): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=port, + ) + + self.assertEqual(port.calls, []) + + def test_authority_must_bind_exact_response_and_offer_digests(self) -> None: + """Authoritative mapping must echo the exact immutable response and offer provenance.""" + for field_name in ("offer_response_digest", "offer_approval_digest", "offer_terms_digest"): + authority = RecordingAuthority() + authority.override[field_name] = "e" * 64 + port = RecordingHirePort() + with self.subTest(field_name=field_name), self.assertRaises(OfferToHireIntegrityError): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=port, + ) + self.assertEqual(port.calls, []) + + def test_authority_time_response_mutation_blocks_hire(self) -> None: + """Mutating response evidence during authority work must not authorize a hire.""" + packet = response() + authority = RecordingAuthority() + authority.mutate_response = lambda: object.__setattr__(packet, "offer_terms_digest", "f" * 64) + port = RecordingHirePort() + + with self.assertRaisesRegex(OfferToHireIntegrityError, "changed during authoritative verification"): + close_accepted_offer_to_hire( + response=packet, + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=port, + ) + + self.assertEqual(port.calls, []) + + def test_verification_runtime_subclass_cannot_cross_trust_boundary(self) -> None: + """Caller-defined verification subtypes cannot become authoritative evidence.""" + class ForgedVerification(CandidateOfferHireVerification): + """Represent an untrusted subtype that must be rejected.""" + + class ForgedAuthority(RecordingAuthority): + """Return a forged verification subtype.""" + + def verify_offer_acceptance(self, **scope: object) -> CandidateOfferHireVerification: + """Construct the otherwise-valid forged subtype.""" + value = super().verify_offer_acceptance(**scope) + return ForgedVerification( + tenant_record_id=value.tenant_record_id, + candidate_profile_id=value.candidate_profile_id, + selection_decision_id=value.selection_decision_id, + offer_response_digest=value.offer_response_digest, + offer_approval_digest=value.offer_approval_digest, + offer_terms_digest=value.offer_terms_digest, + candidate_actor_reference=value.candidate_actor_reference, + authority_evidence_reference=value.authority_evidence_reference, + authority_evidence_digest=value.authority_evidence_digest, + ) + + with self.assertRaisesRegex(TypeError, "exact CandidateOfferHireVerification"): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=ForgedAuthority(), + mutation_port=RecordingHirePort(), + ) + + +if __name__ == "__main__": + unittest.main() From 82ea57e7e90db06b7f85a53a17afc7fdb28be052 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:08:35 -0700 Subject: [PATCH 02/21] test(talent): run exact-head offer-to-hire close gate --- .../workflows/offer-to-hire-close-quality.yml | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/offer-to-hire-close-quality.yml diff --git a/.github/workflows/offer-to-hire-close-quality.yml b/.github/workflows/offer-to-hire-close-quality.yml new file mode 100644 index 000000000..da546fdf6 --- /dev/null +++ b/.github/workflows/offer-to-hire-close-quality.yml @@ -0,0 +1,65 @@ +name: Offer To Hire Close Quality + +on: + pull_request: + paths: + - "services/people-api/src/orgmetra_people_api/offer_close.py" + - "services/people-api/src/orgmetra_people_api/hire.py" + - "services/people-api/src/orgmetra_people_api/auth.py" + - "services/people-api/tests/test_offer_to_hire_close.py" + - "packages/candidate-offer-response/**" + - "packages/keyverse-adapter/**" + - ".github/requirements/foundation-test.txt" + - ".github/workflows/offer-to-hire-close-quality.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: offer-to-hire-close-quality-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + contract: + name: Offer acceptance to confirmed-hire contract + 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 exact Python runtime + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.14.7" + 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 governed boundary + run: | + python -m compileall -q \ + services/people-api/src \ + packages/keyverse-adapter/src \ + packages/candidate-offer-response/src \ + services/people-api/tests/test_offer_to_hire_close.py + - name: Test exact offer-to-hire boundary with 100% owned coverage + env: + PYTHONPATH: services/people-api/src:packages/keyverse-adapter/src:packages/candidate-offer-response/src:packages/hris-kernel/src + COVERAGE_FILE: /tmp/orgmetra-offer-to-hire-close.coverage + run: >- + python -m pytest + -o 'addopts=--cov=orgmetra_people_api.offer_close --cov-branch --cov-report=term-missing --cov-fail-under=100' + services/people-api/tests/test_offer_to_hire_close.py + - name: Require clean checkout + run: | + git diff --exit-code + test -z "$(git status --porcelain)" From 92bd07db9067bda4bbf1ebccda1918edbc982563 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:09:57 -0700 Subject: [PATCH 03/21] feat(talent): govern accepted offer to confirmed hire --- .../src/orgmetra_people_api/offer_close.py | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 services/people-api/src/orgmetra_people_api/offer_close.py diff --git a/services/people-api/src/orgmetra_people_api/offer_close.py b/services/people-api/src/orgmetra_people_api/offer_close.py new file mode 100644 index 000000000..bcaab9bd9 --- /dev/null +++ b/services/people-api/src/orgmetra_people_api/offer_close.py @@ -0,0 +1,253 @@ +"""Connect candidate acceptance evidence to authoritative confirmed-hire materialization. + +Candidate response evidence is a necessary candidate-originated fact, never hire +authority. This boundary snapshots an exact accepted response, asks an injected +authoritative host to resolve that response to one candidate profile and one immutable +selection decision, verifies the returned scope, then delegates to the existing +purpose-bound ``accept_confirmed_hire`` path. No candidate PII or compensation value is +copied into this orchestration boundary. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from hashlib import sha256 +import json +import re +from typing import Protocol, runtime_checkable +from uuid import UUID + +from orgmetra_candidate_offer_response import CandidateOfferResponsePacket +from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy + +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import ( + HireAcceptanceCommand, + HireAcceptancePort, + HireAcceptanceResult, + accept_confirmed_hire, +) + +_MAX_UUID_INT = (1 << 128) - 1 +_DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") +_REFERENCE_PATTERN = re.compile(r"^[a-z][a-z0-9_]*:[A-Za-z0-9][A-Za-z0-9._~-]*$") + + +class OfferToHireIntegrityError(RuntimeError): + """Indicate that offer-response evidence cannot safely authorize hire orchestration.""" + + +def _validate_operational_uuid(value: object, field_name: str) -> None: + """Require an exact non-sentinel UUID from authoritative Orgmetra resolution.""" + if type(value) is not UUID or value.int in (0, _MAX_UUID_INT): + raise ValueError(f"{field_name} must be an exact operational UUID") + + +def _validate_digest(value: object, field_name: str) -> None: + """Require exact built-in lowercase SHA-256 hexadecimal evidence.""" + if type(value) is not str or _DIGEST_PATTERN.fullmatch(value) is None: + raise ValueError(f"{field_name} must be lowercase SHA-256 hex") + + +def _validate_candidate_actor(value: object) -> None: + """Require the bounded external candidate subject already reviewed by the response contract.""" + if ( + type(value) is not str + or len(value) > 288 + or _REFERENCE_PATTERN.fullmatch(value) is None + or not value.startswith("candidate:") + ): + raise ValueError("candidate_actor_reference must be a bounded candidate: opaque reference") + + +def _validate_authority_reference(value: object) -> None: + """Require one Orgmetra-owned opaque UUIDv4 authority evidence reference.""" + message = "authority_evidence_reference must be an opaque offer_hire_verification: UUIDv4 reference" + if ( + type(value) is not str + or len(value) > 160 + or _REFERENCE_PATTERN.fullmatch(value) is None + or not value.startswith("offer_hire_verification:") + ): + raise ValueError(message) + suffix = value.split(":", 1)[1] + try: + parsed = UUID(suffix) + except (ValueError, AttributeError, TypeError) as error: + raise ValueError(message) from error + if str(parsed) != suffix or parsed.version != 4 or parsed.int in (0, _MAX_UUID_INT): + raise ValueError(message) + + +@dataclass(frozen=True, slots=True, repr=False) +class CandidateOfferHireVerification: + """PII-minimized evidence that an authoritative host resolved one accepted offer to hire scope.""" + + tenant_record_id: UUID + candidate_profile_id: UUID + selection_decision_id: UUID + offer_response_digest: str + offer_approval_digest: str + offer_terms_digest: str + candidate_actor_reference: str + authority_evidence_reference: str + authority_evidence_digest: str + + def __repr__(self) -> str: + """Avoid emitting candidate, selection, offer, or authority correlation in routine logs.""" + return "CandidateOfferHireVerification()" + + def __post_init__(self) -> None: + """Reject malformed authoritative evidence before the hire path can consume it.""" + self.validate_live() + + def validate_live(self) -> None: + """Revalidate fields so post-construction rewriting cannot cross the trust boundary.""" + _validate_operational_uuid(self.tenant_record_id, "tenant_record_id") + _validate_operational_uuid(self.candidate_profile_id, "candidate_profile_id") + _validate_operational_uuid(self.selection_decision_id, "selection_decision_id") + _validate_digest(self.offer_response_digest, "offer_response_digest") + _validate_digest(self.offer_approval_digest, "offer_approval_digest") + _validate_digest(self.offer_terms_digest, "offer_terms_digest") + _validate_candidate_actor(self.candidate_actor_reference) + _validate_authority_reference(self.authority_evidence_reference) + _validate_digest(self.authority_evidence_digest, "authority_evidence_digest") + + +@runtime_checkable +class CandidateOfferHireAuthority(Protocol): + """Resolve one candidate response to exact authoritative candidate and selection scope.""" + + def verify_offer_acceptance( + self, + *, + tenant_record_id: str, + candidate_profile_reference: str, + selection_decision_id: UUID, + offer_response_reference: str, + offer_response_digest: str, + candidate_actor_reference: str, + identity_resolution_reference: str, + identity_resolution_digest: str, + offer_approval_reference: str, + offer_approval_digest: str, + offer_terms_reference: str, + offer_terms_digest: str, + responded_at: str, + ) -> CandidateOfferHireVerification: + """Return exact-scope evidence only after authoritative identity/offer resolution succeeds.""" + + +def _snapshot_response(response: CandidateOfferResponsePacket) -> tuple[str, str, dict[str, object]]: + """Freeze one verified candidate-response representation before authoritative host work.""" + if type(response) is not CandidateOfferResponsePacket: + raise TypeError("response must be the exact CandidateOfferResponsePacket runtime type") + try: + canonical_json = response.canonical_json() + except (KeyError, ValueError) as error: + raise OfferToHireIntegrityError("candidate offer response evidence is not intact") from error + payload = json.loads(canonical_json) + return canonical_json, sha256(canonical_json.encode("utf-8")).hexdigest(), payload + + +def _snapshot_verification(value: CandidateOfferHireVerification) -> CandidateOfferHireVerification: + """Copy and revalidate exact authority evidence before comparing it with requested scope.""" + if type(value) is not CandidateOfferHireVerification: + raise TypeError("authority must return the exact CandidateOfferHireVerification runtime type") + return CandidateOfferHireVerification( + tenant_record_id=value.tenant_record_id, + candidate_profile_id=value.candidate_profile_id, + selection_decision_id=value.selection_decision_id, + offer_response_digest=value.offer_response_digest, + offer_approval_digest=value.offer_approval_digest, + offer_terms_digest=value.offer_terms_digest, + candidate_actor_reference=value.candidate_actor_reference, + authority_evidence_reference=value.authority_evidence_reference, + authority_evidence_digest=value.authority_evidence_digest, + ) + + +def close_accepted_offer_to_hire( + *, + response: CandidateOfferResponsePacket, + principal: AuthenticatedPrincipal, + command: HireAcceptanceCommand, + purpose_code: str, + policy: PurposeBoundAccessPolicy, + authority: CandidateOfferHireAuthority, + mutation_port: HireAcceptancePort, +) -> HireAcceptanceResult: + """Require exact candidate acceptance and authority mapping before confirmed-hire mutation. + + The candidate response cannot authorize employment creation by itself. The injected + authority must re-resolve the external candidate subject, candidate profile, exact offer + approval/terms provenance, response authority, and its mapping to the immutable selection + decision supplied by ``command``. Only then does this function invoke the existing + purpose-bound confirmed-hire service, which independently authorizes that selection + decision before any People/Employment/conversion persistence occurs. + """ + if type(command) is not HireAcceptanceCommand: + raise TypeError("command must be the exact HireAcceptanceCommand runtime type") + if not isinstance(authority, CandidateOfferHireAuthority): + raise TypeError("authority must implement CandidateOfferHireAuthority") + + response_json, response_digest, payload = _snapshot_response(response) + if payload.get("response_code") != "offer_accepted": + raise OfferToHireIntegrityError("candidate offer response must be accepted before hire orchestration") + if payload.get("tenant_record_id") != str(command.tenant_record_id): + raise OfferToHireIntegrityError("candidate response tenant does not match the hire command") + + verification = authority.verify_offer_acceptance( + tenant_record_id=str(payload["tenant_record_id"]), + candidate_profile_reference=str(payload["candidate_profile_reference"]), + selection_decision_id=command.selection_decision_id, + offer_response_reference=str(payload["offer_response_reference"]), + offer_response_digest=response_digest, + candidate_actor_reference=str(payload["candidate_actor_reference"]), + identity_resolution_reference=str(payload["identity_resolution_reference"]), + identity_resolution_digest=str(payload["identity_resolution_digest"]), + offer_approval_reference=str(payload["offer_approval_reference"]), + offer_approval_digest=str(payload["offer_approval_digest"]), + offer_terms_reference=str(payload["offer_terms_reference"]), + offer_terms_digest=str(payload["offer_terms_digest"]), + responded_at=str(payload["responded_at"]), + ) + verified = _snapshot_verification(verification) + + if verified.tenant_record_id != command.tenant_record_id: + raise OfferToHireIntegrityError("authority tenant does not match the hire command") + if verified.candidate_profile_id != command.candidate_profile_id: + raise OfferToHireIntegrityError("authority candidate profile does not match the hire command") + if verified.selection_decision_id != command.selection_decision_id: + raise OfferToHireIntegrityError("authority selection decision does not match the hire command") + expected_evidence = ( + response_digest, + payload["offer_approval_digest"], + payload["offer_terms_digest"], + payload["candidate_actor_reference"], + ) + verified_evidence = ( + verified.offer_response_digest, + verified.offer_approval_digest, + verified.offer_terms_digest, + verified.candidate_actor_reference, + ) + if verified_evidence != expected_evidence: + raise OfferToHireIntegrityError("authority evidence does not match the accepted offer response") + + try: + post_authority_json = response.canonical_json() + except (KeyError, ValueError) as error: + raise OfferToHireIntegrityError( + "candidate offer response changed during authoritative verification" + ) from error + if post_authority_json != response_json: + raise OfferToHireIntegrityError("candidate offer response changed during authoritative verification") + + return accept_confirmed_hire( + principal=principal, + command=command, + purpose_code=purpose_code, + policy=policy, + mutation_port=mutation_port, + ) From 8e673ce86597ef3e43906b101645a55c099f9558 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:10:10 -0700 Subject: [PATCH 04/21] feat(talent): export offer-to-hire close boundary --- .../people-api/src/orgmetra_people_api/__init__.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/services/people-api/src/orgmetra_people_api/__init__.py b/services/people-api/src/orgmetra_people_api/__init__.py index b043bed33..6425dd2b3 100644 --- a/services/people-api/src/orgmetra_people_api/__init__.py +++ b/services/people-api/src/orgmetra_people_api/__init__.py @@ -1,4 +1,4 @@ -"""Request-edge, governed read, confirmed-hire, and People mutation contracts.""" +"""Request-edge, governed read, confirmed-hire, offer-close, and People mutation contracts.""" from orgmetra_people_api.auth import ( AuthenticatedPrincipal, @@ -32,6 +32,12 @@ create_employment_record, create_position_record, ) +from orgmetra_people_api.offer_close import ( + CandidateOfferHireAuthority, + CandidateOfferHireVerification, + OfferToHireIntegrityError, + close_accepted_offer_to_hire, +) from orgmetra_people_api.people import ( AuthorizedWorkerPeopleView, PeopleReadPort, @@ -48,12 +54,15 @@ "AuthenticatedPrincipal", "AuthenticationFailed", "AuthorizedWorkerPeopleView", + "CandidateOfferHireAuthority", + "CandidateOfferHireVerification", "HireAcceptanceCommand", "HireAcceptancePort", "HireAcceptanceResult", "HireAcceptanceAsgiApp", "HireDecisionIntegrityError", "HireDecisionNotFound", + "OfferToHireIntegrityError", "PeopleAsgiApp", "PeopleMutationAsgiApp", "PeopleMutationIntegrityError", @@ -75,6 +84,7 @@ "WorkerPeopleRecord", "accept_confirmed_hire", "authorize_resource_fields", + "close_accepted_offer_to_hire", "create_assignment_record", "create_employment_record", "create_position_record", From 47b6039197fa22ee84351f971c8bf7a836c5d644 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:10:29 -0700 Subject: [PATCH 05/21] build(talent): declare candidate response dependency --- services/people-api/pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/services/people-api/pyproject.toml b/services/people-api/pyproject.toml index 3f228e11c..32f4d3def 100644 --- a/services/people-api/pyproject.toml +++ b/services/people-api/pyproject.toml @@ -11,6 +11,7 @@ requires-python = ">=3.11" license = { text = "Apache-2.0" } authors = [{ name = "ContextualWisdomLab" }] dependencies = [ + "orgmetra-candidate-offer-response==0.1.0", "orgmetra-hris-kernel==0.1.0", "orgmetra-keyverse-adapter==0.1.0", ] From 9dfc1a0dfa050ad1e553db72793bcf9746ef84aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:10:50 -0700 Subject: [PATCH 06/21] test(talent): cover public offer-close packaging boundary --- .github/workflows/offer-to-hire-close-quality.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/offer-to-hire-close-quality.yml b/.github/workflows/offer-to-hire-close-quality.yml index da546fdf6..0d4bb6d10 100644 --- a/.github/workflows/offer-to-hire-close-quality.yml +++ b/.github/workflows/offer-to-hire-close-quality.yml @@ -3,6 +3,8 @@ name: Offer To Hire Close Quality on: pull_request: paths: + - "services/people-api/pyproject.toml" + - "services/people-api/src/orgmetra_people_api/__init__.py" - "services/people-api/src/orgmetra_people_api/offer_close.py" - "services/people-api/src/orgmetra_people_api/hire.py" - "services/people-api/src/orgmetra_people_api/auth.py" @@ -50,6 +52,7 @@ jobs: services/people-api/src \ packages/keyverse-adapter/src \ packages/candidate-offer-response/src \ + packages/hris-kernel/src \ services/people-api/tests/test_offer_to_hire_close.py - name: Test exact offer-to-hire boundary with 100% owned coverage env: @@ -59,6 +62,18 @@ jobs: python -m pytest -o 'addopts=--cov=orgmetra_people_api.offer_close --cov-branch --cov-report=term-missing --cov-fail-under=100' services/people-api/tests/test_offer_to_hire_close.py + - name: Require public export and declared candidate-response dependency + env: + PYTHONPATH: services/people-api/src:packages/keyverse-adapter/src:packages/candidate-offer-response/src:packages/hris-kernel/src + run: | + python - <<'PY' + import orgmetra_people_api + from orgmetra_people_api import close_accepted_offer_to_hire + + assert close_accepted_offer_to_hire is orgmetra_people_api.close_accepted_offer_to_hire + pyproject = open("services/people-api/pyproject.toml", encoding="utf-8").read() + assert '"orgmetra-candidate-offer-response==0.1.0"' in pyproject + PY - name: Require clean checkout run: | git diff --exit-code From 37f45b7100fd4e7516ad50a8b99f8c6afb4e4153 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:13:10 -0700 Subject: [PATCH 07/21] test(talent): harden offer-to-hire integrity regressions --- .../tests/test_offer_to_hire_close.py | 191 +++++++++++++++--- 1 file changed, 168 insertions(+), 23 deletions(-) diff --git a/services/people-api/tests/test_offer_to_hire_close.py b/services/people-api/tests/test_offer_to_hire_close.py index 3185528a3..b48d1c300 100644 --- a/services/people-api/tests/test_offer_to_hire_close.py +++ b/services/people-api/tests/test_offer_to_hire_close.py @@ -2,7 +2,6 @@ from __future__ import annotations -from dataclasses import replace from datetime import date, datetime, timezone import unittest from uuid import UUID @@ -19,7 +18,9 @@ ) TENANT = UUID("0198a412-7000-7000-8000-000000000001") +OTHER_TENANT = UUID("0198a412-7000-7000-8000-000000000099") CANDIDATE = UUID("0198a412-7000-7000-8000-000000000010") +OTHER_CANDIDATE = UUID("0198a412-7000-7000-8000-000000000098") DECISION = UUID("0198a412-7000-7000-8000-000000000011") PERSON = UUID("0198a412-7000-7000-8000-000000000020") PERSON_NAME = UUID("0198a412-7000-7000-8000-000000000021") @@ -42,10 +43,10 @@ RECORDED_AT = datetime(2026, 8, 24, 7, 1, tzinfo=timezone.utc) -def response(*, response_code: str = "offer_accepted"): +def response(*, response_code: str = "offer_accepted", tenant_record_id: str | None = None): """Build one value-minimized candidate offer response.""" return build_candidate_offer_response( - tenant_record_id=str(TENANT), + tenant_record_id=tenant_record_id or str(TENANT), offer_response_reference=RESPONSE_REFERENCE, candidate_profile_reference=CANDIDATE_PROFILE_REFERENCE, offer_approval_reference=OFFER_APPROVAL_REFERENCE, @@ -103,6 +104,7 @@ class RecordingHirePort: """Capture authoritative hire calls without persisting HR data.""" def __init__(self) -> None: + """Start with no persistence calls.""" self.calls: list[HireAcceptanceCommand] = [] def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: @@ -120,6 +122,7 @@ class RecordingAuthority: """Resolve candidate-response evidence to exact authoritative hire scope.""" def __init__(self) -> None: + """Start with no authority calls and no forged output overrides.""" self.calls: list[dict[str, object]] = [] self.mutate_response = None self.override: dict[str, object] = {} @@ -144,6 +147,23 @@ def verify_offer_acceptance(self, **scope: object) -> CandidateOfferHireVerifica return CandidateOfferHireVerification(**values) # type: ignore[arg-type] +def valid_verification(**overrides: object) -> CandidateOfferHireVerification: + """Build one exact authoritative verification for constructor-integrity tests.""" + values: dict[str, object] = { + "tenant_record_id": TENANT, + "candidate_profile_id": CANDIDATE, + "selection_decision_id": DECISION, + "offer_response_digest": DIGEST_C, + "offer_approval_digest": DIGEST_A, + "offer_terms_digest": DIGEST_B, + "candidate_actor_reference": "candidate:subject-17", + "authority_evidence_reference": AUTHORITY_EVIDENCE_REFERENCE, + "authority_evidence_digest": DIGEST_D, + } + values.update(overrides) + return CandidateOfferHireVerification(**values) # type: ignore[arg-type] + + class OfferToHireCloseTests(unittest.TestCase): """Prove candidate response is necessary evidence but never hire authority itself.""" @@ -193,31 +213,40 @@ def test_accepted_offer_requires_authoritative_mapping_before_hire(self) -> None self.assertEqual(scope["offer_response_digest"], packet.sha256_digest()) self.assertEqual(scope["offer_approval_digest"], DIGEST_A) self.assertEqual(scope["offer_terms_digest"], DIGEST_B) + self.assertEqual(repr(valid_verification()), "CandidateOfferHireVerification()") def test_authority_scope_mismatch_blocks_hire(self) -> None: """A response-to-selection mapping cannot be widened after authority verification.""" - authority = RecordingAuthority() - authority.override["selection_decision_id"] = UUID("0198a412-7000-7000-8000-000000000099") - port = RecordingHirePort() - - with self.assertRaisesRegex(OfferToHireIntegrityError, "selection decision"): - close_accepted_offer_to_hire( - response=response(), - principal=PRINCIPAL, - command=command(), - purpose_code="candidate_hire", - policy=POLICY, - authority=authority, - mutation_port=port, - ) - - self.assertEqual(port.calls, []) + for field_name, value, message in ( + ("tenant_record_id", OTHER_TENANT, "tenant"), + ("candidate_profile_id", OTHER_CANDIDATE, "candidate profile"), + ("selection_decision_id", UUID("0198a412-7000-7000-8000-000000000097"), "selection decision"), + ): + authority = RecordingAuthority() + authority.override[field_name] = value + port = RecordingHirePort() + with self.subTest(field_name=field_name), self.assertRaisesRegex(OfferToHireIntegrityError, message): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=port, + ) + self.assertEqual(port.calls, []) - def test_authority_must_bind_exact_response_and_offer_digests(self) -> None: - """Authoritative mapping must echo the exact immutable response and offer provenance.""" - for field_name in ("offer_response_digest", "offer_approval_digest", "offer_terms_digest"): + def test_authority_must_bind_exact_response_and_offer_evidence(self) -> None: + """Authoritative mapping must echo exact immutable response, offer, and candidate provenance.""" + for field_name, value in ( + ("offer_response_digest", "e" * 64), + ("offer_approval_digest", "e" * 64), + ("offer_terms_digest", "e" * 64), + ("candidate_actor_reference", "candidate:other-subject"), + ): authority = RecordingAuthority() - authority.override[field_name] = "e" * 64 + authority.override[field_name] = value port = RecordingHirePort() with self.subTest(field_name=field_name), self.assertRaises(OfferToHireIntegrityError): close_accepted_offer_to_hire( @@ -251,6 +280,77 @@ def test_authority_time_response_mutation_blocks_hire(self) -> None: self.assertEqual(port.calls, []) + def test_preexisting_response_tamper_fails_before_authority(self) -> None: + """A response rewritten before orchestration must fail before authority work.""" + packet = response() + object.__setattr__(packet, "offer_terms_digest", "f" * 64) + authority = RecordingAuthority() + + with self.assertRaisesRegex(OfferToHireIntegrityError, "not intact"): + close_accepted_offer_to_hire( + response=packet, + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=RecordingHirePort(), + ) + + self.assertEqual(authority.calls, []) + + def test_command_and_response_tenant_must_match_before_authority(self) -> None: + """A foreign-tenant command cannot reuse candidate response evidence.""" + authority = RecordingAuthority() + + with self.assertRaisesRegex(OfferToHireIntegrityError, "tenant"): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=command(tenant_record_id=OTHER_TENANT), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=RecordingHirePort(), + ) + + self.assertEqual(authority.calls, []) + + def test_untrusted_runtime_types_fail_before_authoritative_work(self) -> None: + """Exact command/response types and one authority protocol are mandatory.""" + authority = RecordingAuthority() + with self.assertRaisesRegex(TypeError, "response must be the exact"): + close_accepted_offer_to_hire( + response=object(), # type: ignore[arg-type] + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=RecordingHirePort(), + ) + with self.assertRaisesRegex(TypeError, "command must be the exact"): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=object(), # type: ignore[arg-type] + purpose_code="candidate_hire", + policy=POLICY, + authority=authority, + mutation_port=RecordingHirePort(), + ) + with self.assertRaisesRegex(TypeError, "authority must implement"): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=object(), # type: ignore[arg-type] + mutation_port=RecordingHirePort(), + ) + self.assertEqual(authority.calls, []) + def test_verification_runtime_subclass_cannot_cross_trust_boundary(self) -> None: """Caller-defined verification subtypes cannot become authoritative evidence.""" class ForgedVerification(CandidateOfferHireVerification): @@ -285,6 +385,51 @@ def verify_offer_acceptance(self, **scope: object) -> CandidateOfferHireVerifica mutation_port=RecordingHirePort(), ) + def test_verification_constructor_rejects_malformed_authority_evidence(self) -> None: + """Every trust-bearing authority field must fail closed before orchestration.""" + invalid_values = ( + {"tenant_record_id": UUID(int=0)}, + {"candidate_profile_id": "not-a-uuid"}, + {"selection_decision_id": UUID(int=(1 << 128) - 1)}, + {"offer_response_digest": "A" * 64}, + {"offer_approval_digest": 17}, + {"offer_terms_digest": "short"}, + {"candidate_actor_reference": "actor:wrong-owner"}, + {"candidate_actor_reference": "candidate:" + "x" * 300}, + {"authority_evidence_reference": 17}, + {"authority_evidence_reference": "offer_hire_verification:not-a-uuid"}, + {"authority_evidence_reference": "offer_hire_verification:6ba7b810-9dad-11d1-80b4-00c04fd430c8"}, + {"authority_evidence_digest": "z" * 64}, + ) + for overrides in invalid_values: + with self.subTest(overrides=overrides), self.assertRaises(ValueError): + valid_verification(**overrides) + + def test_post_construction_verification_rewrite_is_revalidated(self) -> None: + """Frozen verification evidence rewritten with object primitives must still fail closed.""" + authority = RecordingAuthority() + original = authority.verify_offer_acceptance(offer_response_digest=DIGEST_C) + object.__setattr__(original, "authority_evidence_digest", "z" * 64) + + class RewrittenAuthority: + """Return the rewritten verification object.""" + + def verify_offer_acceptance(self, **scope: object) -> CandidateOfferHireVerification: + """Ignore the current request and return the corrupted authority evidence.""" + del scope + return original + + with self.assertRaises(ValueError): + close_accepted_offer_to_hire( + response=response(), + principal=PRINCIPAL, + command=command(), + purpose_code="candidate_hire", + policy=POLICY, + authority=RewrittenAuthority(), + mutation_port=RecordingHirePort(), + ) + if __name__ == "__main__": unittest.main() From df759463f9b2d5557c992f9cff409e4d8b4b79a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:15:44 -0700 Subject: [PATCH 08/21] fix(talent): remove redundant response integrity branch --- .../src/orgmetra_people_api/offer_close.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/services/people-api/src/orgmetra_people_api/offer_close.py b/services/people-api/src/orgmetra_people_api/offer_close.py index bcaab9bd9..fbc337b09 100644 --- a/services/people-api/src/orgmetra_people_api/offer_close.py +++ b/services/people-api/src/orgmetra_people_api/offer_close.py @@ -1,10 +1,10 @@ """Connect candidate acceptance evidence to authoritative confirmed-hire materialization. Candidate response evidence is a necessary candidate-originated fact, never hire -authority. This boundary snapshots an exact accepted response, asks an injected +authority. This boundary snapshots an exact accepted response, asks an injected authoritative host to resolve that response to one candidate profile and one immutable selection decision, verifies the returned scope, then delegates to the existing -purpose-bound ``accept_confirmed_hire`` path. No candidate PII or compensation value is +purpose-bound ``accept_confirmed_hire`` path. No candidate PII or compensation value is copied into this orchestration boundary. """ @@ -50,7 +50,7 @@ def _validate_digest(value: object, field_name: str) -> None: def _validate_candidate_actor(value: object) -> None: - """Require the bounded external candidate subject already reviewed by the response contract.""" + """Require the bounded external candidate subject reviewed by the response contract.""" if ( type(value) is not str or len(value) > 288 @@ -138,7 +138,7 @@ def verify_offer_acceptance( """Return exact-scope evidence only after authoritative identity/offer resolution succeeds.""" -def _snapshot_response(response: CandidateOfferResponsePacket) -> tuple[str, str, dict[str, object]]: +def _snapshot_response(response: CandidateOfferResponsePacket) -> tuple[str, dict[str, object]]: """Freeze one verified candidate-response representation before authoritative host work.""" if type(response) is not CandidateOfferResponsePacket: raise TypeError("response must be the exact CandidateOfferResponsePacket runtime type") @@ -147,7 +147,7 @@ def _snapshot_response(response: CandidateOfferResponsePacket) -> tuple[str, str except (KeyError, ValueError) as error: raise OfferToHireIntegrityError("candidate offer response evidence is not intact") from error payload = json.loads(canonical_json) - return canonical_json, sha256(canonical_json.encode("utf-8")).hexdigest(), payload + return sha256(canonical_json.encode("utf-8")).hexdigest(), payload def _snapshot_verification(value: CandidateOfferHireVerification) -> CandidateOfferHireVerification: @@ -179,10 +179,10 @@ def close_accepted_offer_to_hire( ) -> HireAcceptanceResult: """Require exact candidate acceptance and authority mapping before confirmed-hire mutation. - The candidate response cannot authorize employment creation by itself. The injected + The candidate response cannot authorize employment creation by itself. The injected authority must re-resolve the external candidate subject, candidate profile, exact offer approval/terms provenance, response authority, and its mapping to the immutable selection - decision supplied by ``command``. Only then does this function invoke the existing + decision supplied by ``command``. Only then does this function invoke the existing purpose-bound confirmed-hire service, which independently authorizes that selection decision before any People/Employment/conversion persistence occurs. """ @@ -191,7 +191,7 @@ def close_accepted_offer_to_hire( if not isinstance(authority, CandidateOfferHireAuthority): raise TypeError("authority must implement CandidateOfferHireAuthority") - response_json, response_digest, payload = _snapshot_response(response) + response_digest, payload = _snapshot_response(response) if payload.get("response_code") != "offer_accepted": raise OfferToHireIntegrityError("candidate offer response must be accepted before hire orchestration") if payload.get("tenant_record_id") != str(command.tenant_record_id): @@ -236,13 +236,11 @@ def close_accepted_offer_to_hire( raise OfferToHireIntegrityError("authority evidence does not match the accepted offer response") try: - post_authority_json = response.canonical_json() + response.canonical_json() except (KeyError, ValueError) as error: raise OfferToHireIntegrityError( "candidate offer response changed during authoritative verification" ) from error - if post_authority_json != response_json: - raise OfferToHireIntegrityError("candidate offer response changed during authoritative verification") return accept_confirmed_hire( principal=principal, From 127db00d2aaa8872c26747bfd6cbd11afd0caee2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:16:24 -0700 Subject: [PATCH 09/21] docs(talent): trace offer-to-hire close boundary --- docs/traceability/offer-to-hire-close.md | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/traceability/offer-to-hire-close.md diff --git a/docs/traceability/offer-to-hire-close.md b/docs/traceability/offer-to-hire-close.md new file mode 100644 index 000000000..49ae9beb4 --- /dev/null +++ b/docs/traceability/offer-to-hire-close.md @@ -0,0 +1,33 @@ +# Offer-to-hire close traceability + +## Status + +- **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` already contains the governed `accept_confirmed_hire(...)` path. It authorizes one immutable `selection_decision` with purpose-bound policy before an injected mutation port may materialize Person, Employment, candidate-to-worker conversion, audit, and outbox facts. +- **Parent active PR:** #80 owns `CandidateOfferResponsePacket`, a value-minimized candidate-originated `offer_accepted` / `offer_declined` evidence packet that is explicitly `not_authorized_to_hire`. +- **This active stacked PR:** #108 connects an intact `offer_accepted` packet to the existing confirmed-hire path only after an authoritative host re-resolves candidate identity, candidate profile, exact offer approval/terms provenance, response identity, and the immutable selection decision. +- **Not shipped:** #108 is not protected-main truth and remains dependency-constrained on #80. Its checks/reviews must not be transferred from #80; after #80 integrates, this lane must retarget to fresh `develop` and obtain fresh exact-head People/Foundation/SAST/Security/Recovery evidence. + +## Safety and authority contract + +| Concern | Executable boundary | +|---|---| +| Candidate decline | `close_accepted_offer_to_hire(...)` rejects `offer_declined` before calling the authoritative resolver or hire mutation port. | +| Candidate response is not hire authority | The bridge accepts only canonical `CandidateOfferResponsePacket` evidence, then requires `CandidateOfferHireAuthority.verify_offer_acceptance(...)`; it never writes HR facts directly. | +| Tenant isolation | Candidate-response tenant must equal the `HireAcceptanceCommand` tenant, and the returned authority evidence must bind the same tenant. | +| Candidate linkage | The authority resolves the packet's opaque candidate-profile reference to the exact `candidate_profile_id` in the hire command. | +| Selection-decision linkage | The authority must bind the exact immutable `selection_decision_id` consumed by the protected confirmed-hire authorization path. | +| Offer provenance | Response SHA-256, offer-approval digest, offer-terms digest, and external candidate actor must exactly match the snapshotted candidate response. | +| Concurrent / post-construction response mutation | Candidate-response canonical evidence is verified before authority work and revalidated after it; a mutated packet fails closed before hire materialization. | +| Authority runtime integrity | `CandidateOfferHireVerification` is copied into an exact built-in/runtime-owned verification object and all UUID, digest, actor, and authority-reference fields are revalidated before use. | +| High-impact human/authorization boundary | The bridge delegates final consequential authorization to existing `accept_confirmed_hire(...)`, which independently requires purpose, operation scope, authorized field set, authenticated principal, and exact selection-decision target. | +| PII minimization | The bridge carries only correlation identifiers, evidence digests, candidate actor correlation, and the already-existing hire command. It does not duplicate offer compensation, candidate profile values, assessment scores, or free-form candidate text. | + +## Executable evidence + +`services/people-api/tests/test_offer_to_hire_close.py` defines regressions for decline-before-authority, valid accepted-offer delegation, tenant/candidate/selection mismatches, evidence digest/actor mismatch, response mutation before and during authority work, runtime-type forgery, malformed authority evidence, and post-construction authority-evidence rewriting. + +`.github/workflows/offer-to-hire-close-quality.yml` is the dedicated exact-head gate for this slice. The current stacked PR must remain Draft whenever this gate or any applicable integration gate is absent, queued, pending, cancelled, skipped, neutral, failed, stale, or otherwise non-terminal. A stack-local GREEN result would still not authorize merge before #80 integrates and the descendant is revalidated against fresh protected `develop`. + +## Ownership + +Orgmetra owns this bridge and the existing confirmed-hire application boundary. Keyverse remains the read-only identity owner through its published identity contract; #108 does not mutate Keyverse or query foreign application tables. Offer response remains candidate evidence, not an autonomous or model-derived employment decision. From a51daec54252d502ea77859f0139993df1393ada Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:16:40 -0700 Subject: [PATCH 10/21] docs(talent): doctor offer-to-hire close references --- .../offer-to-hire-close-references.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/doctoring/offer-to-hire-close-references.md diff --git a/docs/doctoring/offer-to-hire-close-references.md b/docs/doctoring/offer-to-hire-close-references.md new file mode 100644 index 000000000..15a874436 --- /dev/null +++ b/docs/doctoring/offer-to-hire-close-references.md @@ -0,0 +1,24 @@ +# Offer-to-hire close references + +Reviewed 2026-08-24. These sources inform the governance principle that a candidate response and prior selection evidence remain evidence inputs to an accountable employer decision process; they do not turn candidate acknowledgement, an assessment score, or an automated signal into employment authority by themselves. + +## Primary / authoritative sources + +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 + +- The EEOC identifies employment tests and other selection procedures as subject to federal anti-discrimination law and directs employers to ensure that selection procedures are properly validated for the positions and purposes for which they are used. +- Orgmetra therefore preserves the existing accountable selection-decision authorization boundary rather than allowing an offer-response packet to bypass it. + +U.S. Equal Employment Opportunity Commission. (n.d.). *Regulations and guidelines*. Retrieved August 24, 2026, from https://www.eeoc.gov/regulations-and-guidelines + +- The current EEOC regulations index identifies 29 C.F.R. Part 1607 as the Uniform Guidelines on Employee Selection Procedures. +- This repository treats the Uniform Guidelines as a governing selection-procedure reference, not as a software certification claim. + +Society for Industrial and Organizational Psychology. (2023, January 21). *Considerations and recommendations for the validation and use of AI-based assessments for employee selection*. https://www.siop.org/wp-content/uploads/legacy/SIOP%20Considerations%20and%20Recommendations%20for%20the%20Validation%20and%20Use%20of%20AI-Based%20Assessments%20for%20Employee%20Selection%20010323.pdf + +- SIOP states that AI-based assessments used for hiring and promotion should meet the same scrutiny and standards applied to traditional employment tests and emphasizes documentation for verification and auditing. +- #108 does not introduce an AI decision path. The reference supports the broader Orgmetra rule that evidence provenance and accountable human/employer authority remain distinct from any evidence-generating mechanism. + +## Repository interpretation + +The cited materials do not prescribe Orgmetra's exact API shape. The software contract is an engineering control derived from the product's high-impact-decision requirements: candidate acceptance is necessary evidence for closing an accepted offer, while the authoritative candidate/offer/selection mapping, purpose-bound authorization, and immutable HR mutation remain separate controlled boundaries. From c733b05424b4b578adbd012aa5538c5820972b52 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:16:49 -0700 Subject: [PATCH 11/21] docs(talent): record offer-to-hire close change --- services/people-api/CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 services/people-api/CHANGELOG.md diff --git a/services/people-api/CHANGELOG.md b/services/people-api/CHANGELOG.md new file mode 100644 index 000000000..ed34bc30c --- /dev/null +++ b/services/people-api/CHANGELOG.md @@ -0,0 +1,15 @@ +# People API changelog + +## Unreleased + +### Added + +- Add `close_accepted_offer_to_hire(...)` as a governed bridge from an intact candidate `offer_accepted` evidence packet to the existing authoritative confirmed-hire path. +- Add `CandidateOfferHireAuthority` and redacted `CandidateOfferHireVerification` contracts so candidate identity, candidate profile, exact offer provenance, and immutable selection decision are re-resolved before any hire materialization. +- Add fail-closed regressions for decline handling, tenant/candidate/selection mismatch, evidence mismatch, response tampering, authority-evidence runtime forgery, and post-construction rewriting. + +### Security / governance + +- Candidate offer response remains necessary but non-authorizing evidence; it cannot directly create Person, Employment, or candidate-to-worker facts. +- Consequential authorization remains in the existing purpose-bound `accept_confirmed_hire(...)` boundary. +- This change is stacked on candidate-offer-response PR #80 and is not protected-main truth until its parent integrates and this descendant is freshly revalidated against protected `develop`. From 5cd23b67c9794fb0de911193adc91d6c75e468c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:17:09 -0700 Subject: [PATCH 12/21] docs(talent): explain accepted-offer hire close --- services/people-api/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/people-api/README.md b/services/people-api/README.md index 548a83446..bba8d2614 100644 --- a/services/people-api/README.md +++ b/services/people-api/README.md @@ -14,6 +14,8 @@ The People API quality workflow is part of this contract and must run for pull r `HireAcceptanceAsgiApp` exposes confirmed-hire materialization as `POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions?purpose=candidate_hire`. Authentication and tenant binding occur before request-body parsing, so an unauthenticated or foreign-tenant caller cannot use body parsing or command construction as an oracle. Authenticated requests then pass the validated `Idempotency-Key`, content-type, JSON/schema, authorization, and governed command checks under a 64 KiB cumulative request-body limit, at most 1024 ASGI request frames, and 128 nested JSON containers below the top-level command object. `PostgresHireAcceptancePort` acquires a transaction-scoped lock for the tenant/route/key before it persists Person, Employment, `candidate_worker_conversion_record`, governed audit/outbox evidence, and `people_mutation_idempotency_record` in one tenant-bound transaction. An exact retry returns the first committed person/employment/conversion identities without repeating necessary PII, audit, or outbox writes; reusing the key for changed command semantics fails closed. The legacy `candidate_worker_link` write path is not used. +`close_accepted_offer_to_hire()` is the governed application bridge between candidate-originated offer response evidence and that existing confirmed-hire boundary. An `offer_declined` packet stops before any authority resolver or People mutation is called. An `offer_accepted` packet is still **not** hire authority: the bridge first snapshots its canonical evidence, then requires an injected `CandidateOfferHireAuthority` to re-resolve the external candidate actor, opaque candidate-profile reference, exact offer-approval and offer-terms provenance, and their mapping to the immutable `selection_decision_id` in the `HireAcceptanceCommand`. The returned `CandidateOfferHireVerification` must exactly match tenant, candidate profile, selection decision, response digest, offer digests, and candidate actor; the candidate packet is revalidated after authority work to detect concurrent or post-construction tampering. Only after those checks does the bridge call `accept_confirmed_hire()`, which independently performs the existing purpose-bound selection-decision authorization before the mutation port can materialize worker truth. This bridge does not duplicate candidate PII, compensation values, assessment scores, or free-form response text, and it never queries a foreign service's application tables. + `PeopleMutationAsgiApp` exposes the governed People mutation API as `POST /v1/employment-records`, `POST /v1/position-records`, and `POST /v1/assignment-records`. Each command requires an idempotency key, tenant/actor/purpose headers, a non-blank accountable decision reason, human confirmation, and versioned evidence. The HTTP boundary enforces the exact OpenAPI evidence-object shape and cardinality, rejects additional fields and duplicate evidence items, and canonicalizes the complete reference/version set independent of array order. It first derives a PII-minimized `evidence_set_v1:` identity and then binds that identity together with the exact validated decision reason into `governance_evidence_v1:`. The free-text reason and raw evidence references are not copied into the portable audit envelope, but any reason/reference/version drift changes the governance binding, the immutable audit correlation evidence, and the durable idempotency command digest. A caller therefore cannot reuse the same key after silently changing the high-impact rationale and receive an incorrect replay. The validated `Idempotency-Key` is copied onto the application command and into `PostgresPeopleMutationPort`. Employment and assignment writes require a current `candidate_worker_conversion_record` (`recorded_to IS NULL`) and reuse `orgmetra_hris_kernel` exclusivity and assignment-coverage checks before the port inserts the authoritative fact, calls `record_audit_outbox_event`, and stores `people_mutation_idempotency_record` in the same transaction. A matching retry returns the first committed identity without a second HRIS, audit, or outbox fact. Successful responses contain only opaque record identifiers. The superseded persistence model must not be restored, and the service must not use direct cross-service application-table SQL. From 6969e4d2d70cda2f89c77f47fa863371b926fc7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:17:25 -0700 Subject: [PATCH 13/21] test(talent): gate offer-close docs and changelog --- .github/workflows/offer-to-hire-close-quality.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/offer-to-hire-close-quality.yml b/.github/workflows/offer-to-hire-close-quality.yml index 0d4bb6d10..35f31db6d 100644 --- a/.github/workflows/offer-to-hire-close-quality.yml +++ b/.github/workflows/offer-to-hire-close-quality.yml @@ -9,8 +9,12 @@ on: - "services/people-api/src/orgmetra_people_api/hire.py" - "services/people-api/src/orgmetra_people_api/auth.py" - "services/people-api/tests/test_offer_to_hire_close.py" + - "services/people-api/README.md" + - "services/people-api/CHANGELOG.md" - "packages/candidate-offer-response/**" - "packages/keyverse-adapter/**" + - "docs/traceability/offer-to-hire-close.md" + - "docs/doctoring/offer-to-hire-close-references.md" - ".github/requirements/foundation-test.txt" - ".github/workflows/offer-to-hire-close-quality.yml" workflow_dispatch: @@ -74,6 +78,12 @@ jobs: pyproject = open("services/people-api/pyproject.toml", encoding="utf-8").read() assert '"orgmetra-candidate-offer-response==0.1.0"' in pyproject PY + - name: Require buyer-readable governance documentation + run: | + grep -F 'CandidateOfferHireAuthority' services/people-api/README.md + grep -F 'not an autonomous or model-derived employment decision' docs/traceability/offer-to-hire-close.md + grep -F 'U.S. Equal Employment Opportunity Commission' docs/doctoring/offer-to-hire-close-references.md + grep -F 'close_accepted_offer_to_hire' services/people-api/CHANGELOG.md - name: Require clean checkout run: | git diff --exit-code From e8a2726bafc34051c9d568b9d1e92ba66f96d774 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:18:22 -0700 Subject: [PATCH 14/21] test(talent): require authorization before offer resolution --- .../test_offer_to_hire_authorization_order.py | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 services/people-api/tests/test_offer_to_hire_authorization_order.py diff --git a/services/people-api/tests/test_offer_to_hire_authorization_order.py b/services/people-api/tests/test_offer_to_hire_authorization_order.py new file mode 100644 index 000000000..9c1e9c355 --- /dev/null +++ b/services/people-api/tests/test_offer_to_hire_authorization_order.py @@ -0,0 +1,122 @@ +"""Regression for authorization ordering in accepted-offer hire orchestration.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +import unittest +from uuid import UUID + +from orgmetra_candidate_offer_response import build_candidate_offer_response +from orgmetra_keyverse_adapter import AuthorizationDeniedError, PurposeBoundAccessPolicy +from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.hire import HireAcceptanceCommand, HireAcceptanceResult +from orgmetra_people_api.offer_close import CandidateOfferHireVerification, close_accepted_offer_to_hire + +TENANT = UUID("0198a412-7000-7000-8000-000000000001") +CANDIDATE = UUID("0198a412-7000-7000-8000-000000000010") +DECISION = UUID("0198a412-7000-7000-8000-000000000011") +DIGEST_A = "a" * 64 +DIGEST_B = "b" * 64 +DIGEST_C = "c" * 64 +DIGEST_D = "d" * 64 + + +class RecordingAuthority: + """Record whether protected candidate/offer resolution was attempted.""" + + def __init__(self) -> None: + """Start with no protected-resolution calls.""" + self.calls = 0 + + def verify_offer_acceptance(self, **scope: object) -> CandidateOfferHireVerification: + """Return valid evidence only if incorrectly reached before denial.""" + self.calls += 1 + return CandidateOfferHireVerification( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=DECISION, + offer_response_digest=str(scope["offer_response_digest"]), + offer_approval_digest=DIGEST_A, + offer_terms_digest=DIGEST_B, + candidate_actor_reference="candidate:subject-17", + authority_evidence_reference="offer_hire_verification:6ba7b815-9dad-41d1-80b4-00c04fd430c8", + authority_evidence_digest=DIGEST_D, + ) + + +class RecordingPort: + """Fail if hire persistence is reached after an authorization denial.""" + + def accept_hire(self, *, command: HireAcceptanceCommand, authorization: object) -> HireAcceptanceResult: + """Raise because denied requests must never reach persistence.""" + del command, authorization + raise AssertionError("denied offer-to-hire request reached persistence") + + +class OfferToHireAuthorizationOrderTests(unittest.TestCase): + """Require purpose-bound authorization before protected offer/candidate resolution.""" + + def test_policy_denial_prevents_authority_resolution(self) -> None: + """Wrong-purpose callers must not invoke the candidate/offer authority resolver.""" + authority = RecordingAuthority() + response = build_candidate_offer_response( + tenant_record_id=str(TENANT), + offer_response_reference="candidate_offer_response:6ba7b811-9dad-41d1-80b4-00c04fd430c8", + candidate_profile_reference="candidate_profile:6ba7b810-9dad-41d1-80b4-00c04fd430c8", + offer_approval_reference="offer_approval:6ba7b812-9dad-41d1-80b4-00c04fd430c8", + offer_approval_digest=DIGEST_A, + offer_terms_reference="offer_terms:6ba7b813-9dad-41d1-80b4-00c04fd430c8", + offer_terms_digest=DIGEST_B, + candidate_actor_reference="candidate:subject-17", + identity_resolution_reference="identity_resolution:6ba7b814-9dad-41d1-80b4-00c04fd430c8", + identity_resolution_digest=DIGEST_C, + response_code="offer_accepted", + responded_at=datetime(2026, 8, 24, 7, 0, tzinfo=timezone.utc), + recorded_at=datetime(2026, 8, 24, 7, 1, tzinfo=timezone.utc), + ) + command = HireAcceptanceCommand( + tenant_record_id=TENANT, + candidate_profile_id=CANDIDATE, + selection_decision_id=DECISION, + person_record_id=UUID("0198a412-7000-7000-8000-000000000020"), + person_name_record_id=UUID("0198a412-7000-7000-8000-000000000021"), + employment_record_id=UUID("0198a412-7000-7000-8000-000000000030"), + employment_record_version_id=UUID("0198a412-7000-7000-8000-000000000031"), + candidate_worker_conversion_record_id=UUID("0198a412-7000-7000-8000-000000000040"), + audit_event_record_id=UUID("0198a412-7000-7000-8000-000000000050"), + outbox_delivery_record_id=UUID("0198a412-7000-7000-8000-000000000051"), + effective_from=date(2026, 8, 25), + display_name="Ada Lovelace", + idempotency_key="offer-close-auth-order-17", + ) + principal = AuthenticatedPrincipal( + tenant_record_id=TENANT, + actor_reference="keyverse_subject:operator-17", + granted_scope_codes=frozenset({"orgmetra.people.materialize_worker"}), + ) + denied_policy = PurposeBoundAccessPolicy( + tenant_record_id=TENANT, + policy_version_code="people-hire-v1", + resource_kind="selection_decision", + purpose_code="benefits_admin", + operation_code="materialize_worker", + required_scope_code="orgmetra.people.materialize_worker", + permitted_fields=frozenset({"candidate_worker_conversion"}), + ) + + with self.assertRaises(AuthorizationDeniedError): + close_accepted_offer_to_hire( + response=response, + principal=principal, + command=command, + purpose_code="candidate_hire", + policy=denied_policy, + authority=authority, + mutation_port=RecordingPort(), + ) + + self.assertEqual(authority.calls, 0) + + +if __name__ == "__main__": + unittest.main() From 21e992df613770c5b269f50c8fc606ec57260e73 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:18:57 -0700 Subject: [PATCH 15/21] fix(talent): authorize before candidate offer resolution --- .../src/orgmetra_people_api/offer_close.py | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/services/people-api/src/orgmetra_people_api/offer_close.py b/services/people-api/src/orgmetra_people_api/offer_close.py index fbc337b09..7e8850917 100644 --- a/services/people-api/src/orgmetra_people_api/offer_close.py +++ b/services/people-api/src/orgmetra_people_api/offer_close.py @@ -1,11 +1,11 @@ """Connect candidate acceptance evidence to authoritative confirmed-hire materialization. Candidate response evidence is a necessary candidate-originated fact, never hire -authority. This boundary snapshots an exact accepted response, asks an injected -authoritative host to resolve that response to one candidate profile and one immutable -selection decision, verifies the returned scope, then delegates to the existing -purpose-bound ``accept_confirmed_hire`` path. No candidate PII or compensation value is -copied into this orchestration boundary. +authority. This boundary first verifies that the authenticated caller is purpose-bound to +materialize the exact selection decision, then snapshots an accepted response, asks an +injected authoritative host to resolve the response to one candidate profile and selection +decision, verifies the returned scope, and delegates to the existing confirmed-hire path. +No candidate PII or compensation value is copied into this orchestration boundary. """ from __future__ import annotations @@ -21,6 +21,7 @@ from orgmetra_keyverse_adapter import PurposeBoundAccessPolicy from orgmetra_people_api.auth import AuthenticatedPrincipal +from orgmetra_people_api.authorization import authorize_resource_fields from orgmetra_people_api.hire import ( HireAcceptanceCommand, HireAcceptancePort, @@ -31,6 +32,7 @@ _MAX_UUID_INT = (1 << 128) - 1 _DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$") _REFERENCE_PATTERN = re.compile(r"^[a-z][a-z0-9_]*:[A-Za-z0-9][A-Za-z0-9._~-]*$") +_HIRE_MUTATION_FIELDS = frozenset({"candidate_worker_conversion"}) class OfferToHireIntegrityError(RuntimeError): @@ -177,20 +179,33 @@ def close_accepted_offer_to_hire( authority: CandidateOfferHireAuthority, mutation_port: HireAcceptancePort, ) -> HireAcceptanceResult: - """Require exact candidate acceptance and authority mapping before confirmed-hire mutation. - - The candidate response cannot authorize employment creation by itself. The injected - authority must re-resolve the external candidate subject, candidate profile, exact offer - approval/terms provenance, response authority, and its mapping to the immutable selection - decision supplied by ``command``. Only then does this function invoke the existing - purpose-bound confirmed-hire service, which independently authorizes that selection - decision before any People/Employment/conversion persistence occurs. + """Require authorization, candidate acceptance, and exact mapping before hire mutation. + + The candidate response cannot authorize employment creation by itself. Before candidate + identity or offer provenance is resolved, the authenticated principal must already be + purpose-bound to materialize the exact immutable selection decision in ``command``. The + injected authority then re-resolves candidate and offer scope and returns matching evidence. + Only after those checks does this function invoke the existing confirmed-hire service, + which independently reauthorizes the same selection decision immediately before the + mutation port may persist Person/Employment/conversion truth. """ if type(command) is not HireAcceptanceCommand: raise TypeError("command must be the exact HireAcceptanceCommand runtime type") if not isinstance(authority, CandidateOfferHireAuthority): raise TypeError("authority must implement CandidateOfferHireAuthority") + authorize_resource_fields( + principal=principal, + tenant_record_id=command.tenant_record_id, + resource_tenant_record_id=command.tenant_record_id, + resource_reference=f"selection_decision:{command.selection_decision_id.hex}", + purpose_code=purpose_code, + operation_code="materialize_worker", + resource_kind="selection_decision", + requested_fields=_HIRE_MUTATION_FIELDS, + policy=policy, + ) + response_digest, payload = _snapshot_response(response) if payload.get("response_code") != "offer_accepted": raise OfferToHireIntegrityError("candidate offer response must be accepted before hire orchestration") From 944e7388ed250ac6ad7467262deb579b35ff6aff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:19:16 -0700 Subject: [PATCH 16/21] test(talent): include authorization-order regression --- .github/workflows/offer-to-hire-close-quality.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/offer-to-hire-close-quality.yml b/.github/workflows/offer-to-hire-close-quality.yml index 35f31db6d..0fbefe145 100644 --- a/.github/workflows/offer-to-hire-close-quality.yml +++ b/.github/workflows/offer-to-hire-close-quality.yml @@ -8,7 +8,8 @@ on: - "services/people-api/src/orgmetra_people_api/offer_close.py" - "services/people-api/src/orgmetra_people_api/hire.py" - "services/people-api/src/orgmetra_people_api/auth.py" - - "services/people-api/tests/test_offer_to_hire_close.py" + - "services/people-api/src/orgmetra_people_api/authorization.py" + - "services/people-api/tests/test_offer_to_hire_*.py" - "services/people-api/README.md" - "services/people-api/CHANGELOG.md" - "packages/candidate-offer-response/**" @@ -57,7 +58,8 @@ jobs: packages/keyverse-adapter/src \ packages/candidate-offer-response/src \ packages/hris-kernel/src \ - services/people-api/tests/test_offer_to_hire_close.py + services/people-api/tests/test_offer_to_hire_close.py \ + services/people-api/tests/test_offer_to_hire_authorization_order.py - name: Test exact offer-to-hire boundary with 100% owned coverage env: PYTHONPATH: services/people-api/src:packages/keyverse-adapter/src:packages/candidate-offer-response/src:packages/hris-kernel/src @@ -66,6 +68,7 @@ jobs: python -m pytest -o 'addopts=--cov=orgmetra_people_api.offer_close --cov-branch --cov-report=term-missing --cov-fail-under=100' services/people-api/tests/test_offer_to_hire_close.py + services/people-api/tests/test_offer_to_hire_authorization_order.py - name: Require public export and declared candidate-response dependency env: PYTHONPATH: services/people-api/src:packages/keyverse-adapter/src:packages/candidate-offer-response/src:packages/hris-kernel/src From 9494caf76b1710d3475b458431e66821204068cf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:19:35 -0700 Subject: [PATCH 17/21] docs(talent): trace pre-resolution authorization --- docs/traceability/offer-to-hire-close.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/traceability/offer-to-hire-close.md b/docs/traceability/offer-to-hire-close.md index 49ae9beb4..80e775188 100644 --- a/docs/traceability/offer-to-hire-close.md +++ b/docs/traceability/offer-to-hire-close.md @@ -4,14 +4,15 @@ - **Protected-main truth:** `develop@9e3e4847510e1e612b48474ba42b177b8ed824df` already contains the governed `accept_confirmed_hire(...)` path. It authorizes one immutable `selection_decision` with purpose-bound policy before an injected mutation port may materialize Person, Employment, candidate-to-worker conversion, audit, and outbox facts. - **Parent active PR:** #80 owns `CandidateOfferResponsePacket`, a value-minimized candidate-originated `offer_accepted` / `offer_declined` evidence packet that is explicitly `not_authorized_to_hire`. -- **This active stacked PR:** #108 connects an intact `offer_accepted` packet to the existing confirmed-hire path only after an authoritative host re-resolves candidate identity, candidate profile, exact offer approval/terms provenance, response identity, and the immutable selection decision. +- **This active stacked PR:** #108 first requires the authenticated principal to be purpose-bound to the exact hire selection decision, then connects an intact `offer_accepted` packet to the existing confirmed-hire path only after an authoritative host re-resolves candidate identity, candidate profile, exact offer approval/terms provenance, response identity, and the immutable selection decision. - **Not shipped:** #108 is not protected-main truth and remains dependency-constrained on #80. Its checks/reviews must not be transferred from #80; after #80 integrates, this lane must retarget to fresh `develop` and obtain fresh exact-head People/Foundation/SAST/Security/Recovery evidence. ## Safety and authority contract | Concern | Executable boundary | |---|---| -| Candidate decline | `close_accepted_offer_to_hire(...)` rejects `offer_declined` before calling the authoritative resolver or hire mutation port. | +| Authorization before sensitive resolution | `close_accepted_offer_to_hire(...)` purpose-authorizes the exact `selection_decision` and `candidate_worker_conversion` operation before invoking the candidate/offer authority resolver. Wrong-purpose, wrong-scope, or foreign-tenant callers therefore cannot use protected candidate/offer resolution as an oracle. | +| Candidate decline | With a valid hire authorization context, `offer_declined` stops before the authoritative resolver or hire mutation port. | | Candidate response is not hire authority | The bridge accepts only canonical `CandidateOfferResponsePacket` evidence, then requires `CandidateOfferHireAuthority.verify_offer_acceptance(...)`; it never writes HR facts directly. | | Tenant isolation | Candidate-response tenant must equal the `HireAcceptanceCommand` tenant, and the returned authority evidence must bind the same tenant. | | Candidate linkage | The authority resolves the packet's opaque candidate-profile reference to the exact `candidate_profile_id` in the hire command. | @@ -19,12 +20,14 @@ | Offer provenance | Response SHA-256, offer-approval digest, offer-terms digest, and external candidate actor must exactly match the snapshotted candidate response. | | Concurrent / post-construction response mutation | Candidate-response canonical evidence is verified before authority work and revalidated after it; a mutated packet fails closed before hire materialization. | | Authority runtime integrity | `CandidateOfferHireVerification` is copied into an exact built-in/runtime-owned verification object and all UUID, digest, actor, and authority-reference fields are revalidated before use. | -| High-impact human/authorization boundary | The bridge delegates final consequential authorization to existing `accept_confirmed_hire(...)`, which independently requires purpose, operation scope, authorized field set, authenticated principal, and exact selection-decision target. | +| High-impact human/authorization boundary | The bridge delegates consequential mutation to existing `accept_confirmed_hire(...)`, which independently reauthorizes purpose, operation scope, authorized field set, authenticated principal, and exact selection-decision target immediately before persistence. | | PII minimization | The bridge carries only correlation identifiers, evidence digests, candidate actor correlation, and the already-existing hire command. It does not duplicate offer compensation, candidate profile values, assessment scores, or free-form candidate text. | ## Executable evidence -`services/people-api/tests/test_offer_to_hire_close.py` defines regressions for decline-before-authority, valid accepted-offer delegation, tenant/candidate/selection mismatches, evidence digest/actor mismatch, response mutation before and during authority work, runtime-type forgery, malformed authority evidence, and post-construction authority-evidence rewriting. +`services/people-api/tests/test_offer_to_hire_close.py` defines regressions for decline-before-resolution, valid accepted-offer delegation, tenant/candidate/selection mismatches, evidence digest/actor mismatch, response mutation before and during authority work, runtime-type forgery, malformed authority evidence, and post-construction authority-evidence rewriting. + +`services/people-api/tests/test_offer_to_hire_authorization_order.py` independently proves that a denied purpose-bound request cannot invoke the protected candidate/offer authority resolver or hire persistence boundary. `.github/workflows/offer-to-hire-close-quality.yml` is the dedicated exact-head gate for this slice. The current stacked PR must remain Draft whenever this gate or any applicable integration gate is absent, queued, pending, cancelled, skipped, neutral, failed, stale, or otherwise non-terminal. A stack-local GREEN result would still not authorize merge before #80 integrates and the descendant is revalidated against fresh protected `develop`. From 9df5c8a08823cf767d71cf26d87dc736f33e985a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:20:00 -0700 Subject: [PATCH 18/21] docs(talent): document pre-resolution authorization --- services/people-api/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/people-api/README.md b/services/people-api/README.md index bba8d2614..dc2111e39 100644 --- a/services/people-api/README.md +++ b/services/people-api/README.md @@ -14,7 +14,7 @@ The People API quality workflow is part of this contract and must run for pull r `HireAcceptanceAsgiApp` exposes confirmed-hire materialization as `POST /v1/tenants/{tenant_record_id}/candidate-worker-conversions?purpose=candidate_hire`. Authentication and tenant binding occur before request-body parsing, so an unauthenticated or foreign-tenant caller cannot use body parsing or command construction as an oracle. Authenticated requests then pass the validated `Idempotency-Key`, content-type, JSON/schema, authorization, and governed command checks under a 64 KiB cumulative request-body limit, at most 1024 ASGI request frames, and 128 nested JSON containers below the top-level command object. `PostgresHireAcceptancePort` acquires a transaction-scoped lock for the tenant/route/key before it persists Person, Employment, `candidate_worker_conversion_record`, governed audit/outbox evidence, and `people_mutation_idempotency_record` in one tenant-bound transaction. An exact retry returns the first committed person/employment/conversion identities without repeating necessary PII, audit, or outbox writes; reusing the key for changed command semantics fails closed. The legacy `candidate_worker_link` write path is not used. -`close_accepted_offer_to_hire()` is the governed application bridge between candidate-originated offer response evidence and that existing confirmed-hire boundary. An `offer_declined` packet stops before any authority resolver or People mutation is called. An `offer_accepted` packet is still **not** hire authority: the bridge first snapshots its canonical evidence, then requires an injected `CandidateOfferHireAuthority` to re-resolve the external candidate actor, opaque candidate-profile reference, exact offer-approval and offer-terms provenance, and their mapping to the immutable `selection_decision_id` in the `HireAcceptanceCommand`. The returned `CandidateOfferHireVerification` must exactly match tenant, candidate profile, selection decision, response digest, offer digests, and candidate actor; the candidate packet is revalidated after authority work to detect concurrent or post-construction tampering. Only after those checks does the bridge call `accept_confirmed_hire()`, which independently performs the existing purpose-bound selection-decision authorization before the mutation port can materialize worker truth. This bridge does not duplicate candidate PII, compensation values, assessment scores, or free-form response text, and it never queries a foreign service's application tables. +`close_accepted_offer_to_hire()` is the governed application bridge between candidate-originated offer response evidence and that existing confirmed-hire boundary. Before it resolves candidate identity or offer provenance, it requires the authenticated principal to pass the same purpose-bound `materialize_worker` authorization for the exact immutable selection decision, so a wrong-purpose, wrong-scope, or foreign-tenant caller cannot use protected offer/candidate resolution as an oracle. With a valid hire authorization context, `offer_declined` stops before the authority resolver or People mutation is called. An `offer_accepted` packet is still **not** hire authority: the bridge snapshots its canonical evidence, then requires an injected `CandidateOfferHireAuthority` to re-resolve the external candidate actor, opaque candidate-profile reference, exact offer-approval and offer-terms provenance, and their mapping to the immutable `selection_decision_id` in the `HireAcceptanceCommand`. The returned `CandidateOfferHireVerification` must exactly match tenant, candidate profile, selection decision, response digest, offer digests, and candidate actor; the candidate packet is revalidated after authority work to detect concurrent or post-construction tampering. Only after those checks does the bridge call `accept_confirmed_hire()`, which independently repeats the existing purpose-bound selection-decision authorization immediately before the mutation port can materialize worker truth. This bridge does not duplicate candidate PII, compensation values, assessment scores, or free-form response text, and it never queries a foreign service's application tables. `PeopleMutationAsgiApp` exposes the governed People mutation API as `POST /v1/employment-records`, `POST /v1/position-records`, and `POST /v1/assignment-records`. Each command requires an idempotency key, tenant/actor/purpose headers, a non-blank accountable decision reason, human confirmation, and versioned evidence. The HTTP boundary enforces the exact OpenAPI evidence-object shape and cardinality, rejects additional fields and duplicate evidence items, and canonicalizes the complete reference/version set independent of array order. It first derives a PII-minimized `evidence_set_v1:` identity and then binds that identity together with the exact validated decision reason into `governance_evidence_v1:`. The free-text reason and raw evidence references are not copied into the portable audit envelope, but any reason/reference/version drift changes the governance binding, the immutable audit correlation evidence, and the durable idempotency command digest. A caller therefore cannot reuse the same key after silently changing the high-impact rationale and receive an incorrect replay. The validated `Idempotency-Key` is copied onto the application command and into `PostgresPeopleMutationPort`. Employment and assignment writes require a current `candidate_worker_conversion_record` (`recorded_to IS NULL`) and reuse `orgmetra_hris_kernel` exclusivity and assignment-coverage checks before the port inserts the authoritative fact, calls `record_audit_outbox_event`, and stores `people_mutation_idempotency_record` in the same transaction. A matching retry returns the first committed identity without a second HRIS, audit, or outbox fact. Successful responses contain only opaque record identifiers. From 766472aa90887bae59f48d67843b170123034726 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:20:11 -0700 Subject: [PATCH 19/21] docs(talent): record authorization-order hardening --- services/people-api/CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/people-api/CHANGELOG.md b/services/people-api/CHANGELOG.md index ed34bc30c..4365a2af8 100644 --- a/services/people-api/CHANGELOG.md +++ b/services/people-api/CHANGELOG.md @@ -6,10 +6,10 @@ - Add `close_accepted_offer_to_hire(...)` as a governed bridge from an intact candidate `offer_accepted` evidence packet to the existing authoritative confirmed-hire path. - Add `CandidateOfferHireAuthority` and redacted `CandidateOfferHireVerification` contracts so candidate identity, candidate profile, exact offer provenance, and immutable selection decision are re-resolved before any hire materialization. -- Add fail-closed regressions for decline handling, tenant/candidate/selection mismatch, evidence mismatch, response tampering, authority-evidence runtime forgery, and post-construction rewriting. +- Add fail-closed regressions for decline handling, tenant/candidate/selection mismatch, evidence mismatch, response tampering, authority-evidence runtime forgery, post-construction rewriting, and authorization ordering. ### Security / governance - Candidate offer response remains necessary but non-authorizing evidence; it cannot directly create Person, Employment, or candidate-to-worker facts. -- Consequential authorization remains in the existing purpose-bound `accept_confirmed_hire(...)` boundary. +- Require purpose-bound authorization for the exact `materialize_worker` selection decision before protected candidate/offer authority resolution, then independently reauthorize through existing `accept_confirmed_hire(...)` immediately before persistence. - This change is stacked on candidate-offer-response PR #80 and is not protected-main truth until its parent integrates and this descendant is freshly revalidated against protected `develop`. From d15d20e3533ab486656bde92baafa7138f96165b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:22:09 -0700 Subject: [PATCH 20/21] fix(talent): validate response envelope before pre-resolution authorization --- .../src/orgmetra_people_api/offer_close.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/services/people-api/src/orgmetra_people_api/offer_close.py b/services/people-api/src/orgmetra_people_api/offer_close.py index 7e8850917..f711d5dbd 100644 --- a/services/people-api/src/orgmetra_people_api/offer_close.py +++ b/services/people-api/src/orgmetra_people_api/offer_close.py @@ -1,10 +1,10 @@ """Connect candidate acceptance evidence to authoritative confirmed-hire materialization. Candidate response evidence is a necessary candidate-originated fact, never hire -authority. This boundary first verifies that the authenticated caller is purpose-bound to -materialize the exact selection decision, then snapshots an accepted response, asks an -injected authoritative host to resolve the response to one candidate profile and selection -decision, verifies the returned scope, and delegates to the existing confirmed-hire path. +authority. This boundary validates the supplied response envelope, verifies that the +authenticated caller is purpose-bound to materialize the exact selection decision before +protected candidate/offer resolution, asks an injected authoritative host to resolve the +response, verifies the returned scope, and delegates to the existing confirmed-hire path. No candidate PII or compensation value is copied into this orchestration boundary. """ @@ -179,21 +179,27 @@ def close_accepted_offer_to_hire( authority: CandidateOfferHireAuthority, mutation_port: HireAcceptancePort, ) -> HireAcceptanceResult: - """Require authorization, candidate acceptance, and exact mapping before hire mutation. - - The candidate response cannot authorize employment creation by itself. Before candidate - identity or offer provenance is resolved, the authenticated principal must already be - purpose-bound to materialize the exact immutable selection decision in ``command``. The - injected authority then re-resolves candidate and offer scope and returns matching evidence. - Only after those checks does this function invoke the existing confirmed-hire service, - which independently reauthorizes the same selection decision immediately before the - mutation port may persist Person/Employment/conversion truth. + """Require response integrity, authorization, and exact mapping before hire mutation. + + The candidate response cannot authorize employment creation by itself. The supplied packet + is first validated as immutable candidate-originated evidence and checked against the hire + command tenant. Before candidate identity or offer provenance is resolved, the authenticated + principal must be purpose-bound to materialize the exact immutable selection decision. The + injected authority then re-resolves candidate and offer scope. Only after those checks does + this function invoke the existing confirmed-hire service, which independently reauthorizes + the same selection decision immediately before persistence. """ if type(command) is not HireAcceptanceCommand: raise TypeError("command must be the exact HireAcceptanceCommand runtime type") if not isinstance(authority, CandidateOfferHireAuthority): raise TypeError("authority must implement CandidateOfferHireAuthority") + response_digest, payload = _snapshot_response(response) + if payload.get("response_code") != "offer_accepted": + raise OfferToHireIntegrityError("candidate offer response must be accepted before hire orchestration") + if payload.get("tenant_record_id") != str(command.tenant_record_id): + raise OfferToHireIntegrityError("candidate response tenant does not match the hire command") + authorize_resource_fields( principal=principal, tenant_record_id=command.tenant_record_id, @@ -206,12 +212,6 @@ def close_accepted_offer_to_hire( policy=policy, ) - response_digest, payload = _snapshot_response(response) - if payload.get("response_code") != "offer_accepted": - raise OfferToHireIntegrityError("candidate offer response must be accepted before hire orchestration") - if payload.get("tenant_record_id") != str(command.tenant_record_id): - raise OfferToHireIntegrityError("candidate response tenant does not match the hire command") - verification = authority.verify_offer_acceptance( tenant_record_id=str(payload["tenant_record_id"]), candidate_profile_reference=str(payload["candidate_profile_reference"]), From d465d1cd34ec3eeaee863535a7a4142cd018e06b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 23:26:25 +0900 Subject: [PATCH 21/21] ci(offer-to-hire): watch kernel dependency changes --- .github/workflows/offer-to-hire-close-quality.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/offer-to-hire-close-quality.yml b/.github/workflows/offer-to-hire-close-quality.yml index 0fbefe145..5fbe5e789 100644 --- a/.github/workflows/offer-to-hire-close-quality.yml +++ b/.github/workflows/offer-to-hire-close-quality.yml @@ -14,6 +14,7 @@ on: - "services/people-api/CHANGELOG.md" - "packages/candidate-offer-response/**" - "packages/keyverse-adapter/**" + - "packages/hris-kernel/**" - "docs/traceability/offer-to-hire-close.md" - "docs/doctoring/offer-to-hire-close-references.md" - ".github/requirements/foundation-test.txt"