From 1442f1849c5894500d69823e60bfcd3d644dc683 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 17:57:37 +0000 Subject: [PATCH 1/2] feat: report structured graph preflight refusals --- agent_api.py | 79 +++++- app.py | 13 +- docs/contracts/agent-operations.md | 9 + docs/contracts/project-cli.md | 8 + docs/contracts/run-preflight-publication.md | 48 ++++ docs/contracts/workflow-client.md | 8 + .../execution-ledger.md | 48 ++++ .../acceptance.md | 41 ++++ .../focused.log | 78 ++++++ .../independent-review.json | 208 ++++++++++++++++ .../qualification.json | 75 ++++++ pipeline.py | 81 +++++-- project_cli.py | 8 +- tests/test_agent_project_snapshots.py | 2 +- tests/test_run_preflight_refusals.py | 225 ++++++++++++++++++ tests/test_run_preflight_report.py | 4 +- tuldok_client.py | 3 +- 17 files changed, 912 insertions(+), 26 deletions(-) create mode 100644 docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/acceptance.md create mode 100644 docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/focused.log create mode 100644 docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/independent-review.json create mode 100644 docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/qualification.json create mode 100644 tests/test_run_preflight_refusals.py diff --git a/agent_api.py b/agent_api.py index 021860b9..7e7cd9aa 100644 --- a/agent_api.py +++ b/agent_api.py @@ -37,9 +37,10 @@ class AgentAPIError(Exception): """Safe transport error: never contains a credential or remote traceback.""" - def __init__(self, message, code='invalid', status=400): + def __init__(self, message, code='invalid', status=400, *, preflight_report=None): super().__init__(message) self.code, self.status = code, status + self.preflight_report = copy.deepcopy(preflight_report) def encoded(value): @@ -906,6 +907,24 @@ def model_operation(name,method,path,properties=None,required=None,*,output=None _preflight_result['required'].extend(('preflight_report', 'execution_sha256', 'graph_revision', 'status')) _preflight_report_contract['description'] += ' Return a bounded exact-material preparation report with deferred admission, device, custody and publication checks; no reservation or execution.' _CONTRACTS['graphs.preflight_external'][3] = _preflight_report_contract +# The retained success-only report and installed versions remain unchanged. +PREFLIGHT_REPORT_V2 = copy.deepcopy(PREFLIGHT_REPORT) +PREFLIGHT_REPORT_V2['properties'].update( + format=const('external_preflight_report_v2'), + state={'enum': ['prepared', 'blocked', 'unsupported', 'invalid']}, + execution_sha256={'oneOf': [SHA, {'type': 'null'}]}, + material_sha256={'oneOf': [SHA, {'type': 'null'}]}, + refusal={'oneOf': [{'type': 'null'}, obj({'code': {'enum': [ + 'invalid', 'unsupported', 'unavailable', 'stale', 'conflict', 'too_large', 'storage']}, + 'status': integer(400, 599)})]}) +PREFLIGHT_REPORT_V2['required'].append('refusal') +PREFLIGHT_REPORT_V2['properties']['checks']['items']['properties']['state'] = { + 'enum': ['checked', 'deferred', 'blocked', 'unsupported', 'invalid']} +_preflight_refusal_contract = copy.deepcopy(_preflight_report_contract) +_preflight_refusal_contract['outputSchema']['oneOf'][0]['properties']['result']['properties']['preflight_report'] = PREFLIGHT_REPORT_V2 +_preflight_refusal_contract['outputSchema']['oneOf'][1]['properties']['result']['properties']['preflight_report'] = PREFLIGHT_REPORT_V2 +_preflight_refusal_contract['description'] += ' Preserve typed preparation refusals with bounded request-only reports; unverified material identities remain null.' +_CONTRACTS['graphs.preflight_external'][4] = _preflight_refusal_contract # Managed Torch extends the recipe/runtime variants without rewriting the # independently deployed v2/v3 inspection contracts. _managed_inspection=copy.deepcopy(_CONTRACTS['recipes.inspect'][3]) @@ -1270,8 +1289,55 @@ def validate_output(name, envelope, version=None): or any(source[key] != value[key] for key in ('catalog_id', 'run_id')) or value['original_fault']['source_sha256'] != source['sha256']): raise AgentAPIError('Recovered publication result identity is inconsistent.', 'transport', 502) + selected_version = max(_CONTRACTS[op['operation_id']]) if version is None else version + if (op['operation_id'] == 'graphs.preflight_external' and selected_version == 4 + and 'preflight_report' in envelope['result']): + report = envelope['result']['preflight_report'] + content = lambda value: hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), + ensure_ascii=False, allow_nan=False).encode('utf-8')).hexdigest() + if (report['sha256'] != content({k: v for k, v in report.items() if k != 'sha256'}) + or len({row['code'] for row in report['checks']}) != len(report['checks'])): + raise AgentAPIError('Exact preflight report identity is inconsistent.', 'transport', 502) + if envelope['status'] >= 400: + refusal = report['refusal'] + expected = ('invalid' if refusal and refusal['code'] == 'invalid' else + 'unsupported' if refusal and refusal['code'] == 'unsupported' else 'blocked') + evidence = content({k: v for k, v in report.items() if k not in ('sha256', 'checks')}) + # These are request-only projections of the existing owner boundaries. + # A generic prepare exception cannot assert which nested family failed. + layout = { + 'source_inputs': ('pipeline', 'preparation', 'Successful fresh source and input capture'), + 'runtime_inventory': ('pipeline', 'preparation', 'Successful fresh runtime inventory verification'), + 'host_closure': ('pipeline', 'preparation', 'Successful fresh host identity verification'), + 'output_contract': ('pipeline', 'preparation', 'Successful fresh output and evidence validation'), + 'capture_authority': ('pipeline', 'preparation', 'Current source and input disclosure owners'), + 'authority_admission': ('runs', 'admission', 'Runs atomic admission'), + 'resource_availability': ('runs', 'admission', 'Runs current resource admission'), + 'runtime_ready_go': ('runs', 'dispatch', 'Runs runtime READY/GO'), + 'custody_completion': ('runs', 'execution', 'Runs custody and terminal drainage'), + 'output_publication': ('runs', 'publication', 'Runs durable output publication')} + failed = report['checks'][0]['code'] + if failed == 'storage_capacity': + layout[failed] = ('runs', 'preparation', 'Runs current storage observation') + elif failed == 'material_preparation': + layout[failed] = ('pipeline', 'preparation', 'Pipeline fresh material preparation') + else: + raise AgentAPIError('Preflight refusal has no supported owner boundary.', 'transport', 502) + if (refusal is None or refusal['status'] != envelope['status'] + or refusal['status'] in (401, 403) or refusal['code'] != envelope['result'].get('code') + or report['state'] != expected or report['material_sha256'] is not None + or report['execution_sha256'] is not None + or set(layout) != {row['code'] for row in report['checks']} + or any(row['state'] != (expected if row['code'] == failed else 'deferred') + or (row['owner'], row['phase'], row['boundary']) != layout.get(row['code']) + or row['evidence_sha256'] != evidence + for row in report['checks'])): + raise AgentAPIError('Preflight refusal contradicts its typed outcome or unknown material.', 'transport', 502) + elif report['refusal'] is not None or report['state'] != 'prepared' or any( + row['state'] not in ('checked', 'deferred') for row in report['checks']): + raise AgentAPIError('Prepared preflight report contradicts its outcome.', 'transport', 502) if (op['operation_id'] == 'graphs.preflight_external' - and (version == 3 or version is None) and envelope['status'] < 400): + and selected_version in (3, 4) and envelope['status'] < 400): result = envelope['result']; report = result['preflight_report']; material = result['material'] # Domain content identity uses UTF-8 canonical JSON, independently of ASCII wire encoding. def content_sha(value): @@ -1516,7 +1582,13 @@ def watch_read(): if (type(code) is not str or not re.fullmatch('[a-z][a-z0-9_]{0,79}', code) or self._token in code): raise AgentAPIError('Malformed HTTP error code.', 'transport', 502) + has_report = selection == ('graphs.preflight_external', 4) and 'preflight_report' in value + report = value['preflight_report'] if has_report else None + if has_report and self._token in json.dumps(report, ensure_ascii=False): + raise AgentAPIError('Preflight report contains credential material.', 'transport', 502) value = {'code': code, 'error': value['error'].replace(self._token, '[redacted]')[:1000]} + if has_report: + value['preflight_report'] = report return {'status': response.status, 'result': value} except AgentAPIError: raise @@ -1718,7 +1790,8 @@ def call(self, name, arguments, *, observer=None, observation=None): if (value['run_id'] != arguments['run_id'] or (value['state'] == 'published') != (ref is not None) or ref is not None and (ref['run_id'] != arguments['run_id'] or ref['catalog_id'] != self.catalog_id)): raise AgentAPIError('Publication action result does not match its requested Run/catalog.', 'transport', 502) - if op['operation_id'] == 'graphs.preflight_external' and version == 3 and result['status'] < 400: + if (op['operation_id'] == 'graphs.preflight_external' and version in (3, 4) + and 'preflight_report' in result['result']): report = result['result']['preflight_report'] if (report['graph_id'] != arguments['graph_id'] or report['graph_revision'] != arguments['revision'] or report['catalog_id'] != self.catalog_id): diff --git a/app.py b/app.py index 267a8f98..93864efd 100644 --- a/app.py +++ b/app.py @@ -1167,8 +1167,17 @@ def graph_guard(graph): if parts[2] == 'preflight-external': projects.closed(body, ('revision',)) self._graph_snapshot_contract_access(parts[1],body['revision'],'graphs.preflight_external',2) - if self._contract_selection is not None and self._contract_selection[:2] == ('graphs.preflight_external', 3): - return self.reply(dataset.pipelines.preflight_external(parts[1], body['revision'], contract_version=3)) + if self._contract_selection is not None and self._contract_selection[:2] in ( + ('graphs.preflight_external', 3), ('graphs.preflight_external', 4)): + try: + return self.reply(dataset.pipelines.preflight_external( + parts[1], body['revision'], contract_version=self._contract_selection[1])) + except workbench.WorkbenchError as error: + report = getattr(error, 'preflight_report', None) + if report is None: + raise + return self.reply({'error': str(error), 'code': error.code, + 'preflight_report': report}, error.status) return self.reply(dataset.pipelines.preflight_external(parts[1], body['revision'])) if parts == ['runs']: return self.reply(dataset.runs.submit(body), 201) diff --git a/docs/contracts/agent-operations.md b/docs/contracts/agent-operations.md index 7fe5af8a..fb4455e9 100644 --- a/docs/contracts/agent-operations.md +++ b/docs/contracts/agent-operations.md @@ -69,6 +69,15 @@ and configured catalog. Its `prepared`/`not_reserved` state leaves admission, device READY/GO, actual output validation and publication explicitly deferred. No execution authority, retry or new client dependency is introduced. +Selected `graphs.preflight_external/4` retains those versions and adds the +[versioned refusal projection](run-preflight-publication.md#selected-v4-refusal-reports) +to classified preparation errors. It preserves the original HTTP/code outcome; +null material/execution hashes and deferred checks explicitly represent unknown +facts. AgentClient checks report bounds, hash, owner provenance, typed outcome +and request/catalog correlation on failures as well as successes. Bare early +authorization/contract errors remain valid. CLI and WorkflowClient retain the +validated report without retrying or preparing a submission manifest. + The additive selected-v1 publication operations are documented in [publication transport](run-preflight-publication.md#selected-publication-transport-and-collection). Status/result/segment reads require current inspect authority and exact Ref diff --git a/docs/contracts/project-cli.md b/docs/contracts/project-cli.md index 9d716b48..cde0626c 100644 --- a/docs/contracts/project-cli.md +++ b/docs/contracts/project-cli.md @@ -18,6 +18,14 @@ Definite old-server route absence permits exact frozen legacy fallback. Ordinary auth/server/malformed discovery failures never trigger fallback. Contract selection is internal to the client; there are no schema, telemetry or retry flags to manage. +When negotiated graph preflight v4 returns a classified preparation refusal, +`preflight` exits nonzero and stderr JSON includes its validated +`preflight_report` beside the original code, message and status. Unknown material, +runtime readiness and execution remain explicit. Ordinary early errors and older +contracts keep their existing projection. A refusal does not create a submission +manifest, grant, reservation or retry. See the +[report contract](run-preflight-publication.md#selected-v4-refusal-reports). + An owner issues an agent credential with `inspect,edit,execute` and optional `cancel` scopes. The CLI cannot approve execution. In Pipeline the owner reviews the exact graph preflight, expands **Grant this material to an actor**, selects diff --git a/docs/contracts/run-preflight-publication.md b/docs/contracts/run-preflight-publication.md index d3d868cc..685f01a9 100644 --- a/docs/contracts/run-preflight-publication.md +++ b/docs/contracts/run-preflight-publication.md @@ -45,6 +45,54 @@ actual output sizes and science remain later checks. A preparation refusal keeps its existing typed error and emits no successful report. Structured failure diagnostics and complete M1 preflight/publication recovery are not yet implemented. +### Selected v4 refusal reports + +`graphs.preflight_external/4` extends this same report owner with +`external_preflight_report_v2`. Selected v1–v3 and headerless projections retain +their original contract identities. Successful v4 preparation retains the same +exact material checks, uses the v2 discriminator and has `refusal: null`. +It is still `prepared`, not an execution-readiness or scientific pass. + +After authorized Pipeline entry, classified `WorkbenchError` outcomes (`invalid`, +`unsupported`, `unavailable`, `stale`, `conflict`, `too_large`, `storage`) keep their +original HTTP status, code and message. V4 adds `preflight_report` to that existing +error envelope. Invalid declarations report `invalid`, unsupported meaning reports +`unsupported`, and unavailable/stale/resource outcomes report `blocked`. +The report's `refusal` binds the original code/status. It reports only the first +observed refusal and performs no additional preparation or failure investigation. + +Refused reports bind the requested catalog/graph/revision. Both execution and +material SHA256 fields are null, including after storage refusal or graph drift; +they never attest partial material. Their check evidence hashes bind request and +refusal provenance, not content verification. The generic Pipeline preparation +boundary does not guess a source/runtime/output failure family from error text. +Only the explicit Runs storage call may name `storage_capacity`/Runs provenance. +All ten other required checks remain `deferred`, meaning unknown in that refused +preparation. Device READY/GO, custody, publication and scientific acceptance stay +unobserved. The fixed eleven-row refusal fits the existing sixteen-check bound; +each boundary/detail keeps the existing 160/240-character limits. No unbounded +exception, path, traceback or producer diagnostic is copied into the report. + +Authorization, selected-contract and request-schema failures before owner entry, +unclassified errors and non-Workbench storage failures remain ordinary bare typed +errors. Reporting does not perform graph reads to decorate those refusals. +It neither issues nor consumes grants, reserves a Study attempt, starts a child, +publishes a result nor retries preparation. This graph slice does not add reports +to the separate retained-recipe/Study material path or complete all C1/T1 checks. + +AgentClient preserves only the selected v4 report through error normalization and +validates its closed bounds, content hash, code/status/state, exact owner/phase/ +boundary layout, deferred checks and requested catalog/graph/revision. Explicit +null/malformed reports refuse; an absent report remains valid for early errors. +CLI `preflight` errors retain the validated report in stderr JSON with nonzero +exit status; WorkflowClient preparation raises the same typed error carrying +`preflight_report` and emits no submission manifest. Neither consumer retries. +Current credential content in a received refusal report is rejected without echo. + +The success-only/early pending statements above describe the retained v3 slice. +Publication/indexed-diagnostic implementation below has its own qualification; +this extension changes only graph preparation reporting. + ## Producer and consumer boundaries The HTTP producer validates the selected output. The Python AgentClient validates diff --git a/docs/contracts/workflow-client.md b/docs/contracts/workflow-client.md index 24959b0c..edc4fffb 100644 --- a/docs/contracts/workflow-client.md +++ b/docs/contracts/workflow-client.md @@ -13,6 +13,14 @@ changed identity; it is not a signature, grant or scientific approval. Inspect and retain exported manifests before an effect. Grant issuance, Study budgets, runtime installation, scheduling and scientific acceptance remain server-owned. +`submission_manifest` propagates a negotiated v4 graph preflight refusal as the +same `AgentAPIError` code/status, carrying its validated `preflight_report`. +It creates no manifest and performs no submit, fallback or second preparation. +Early errors and older selected contracts have no report. The +[preflight owner contract](run-preflight-publication.md#selected-v4-refusal-reports) +defines request-only provenance, null material/execution identities and deferred +checks for those refused preparations. + ```python from tuldok_client import WorkflowClient diff --git a/docs/plans/lab-workflow-reliability/execution-ledger.md b/docs/plans/lab-workflow-reliability/execution-ledger.md index 6c9de6e9..5a1db61b 100644 --- a/docs/plans/lab-workflow-reliability/execution-ledger.md +++ b/docs/plans/lab-workflow-reliability/execution-ledger.md @@ -1,5 +1,53 @@ # Lab workflow reliability execution ledger +## 2026-10-20 — Local graph preflight refusal reporting + +The user selected continuation of this canonical plan's C1/T1 structured-refusal +slice after PR114 merged. Local branch +`feature/preflight-refusal-reports-20261020` starts at verified main +`360bbda950cc48ed3c3d9193539039b68e0ac8cb` (tree +`30b951bc8f6fa5fd19750be383f2214aaad59eda`). No publication, merge, deletion, +credential change or expired CLI-authentication retry is selected. + +Pipeline remains the sole preparation/report owner; Runs still owns storage, +admission, process custody and publication. Graph v4 extends the existing report +and typed error envelope, leaving installed v1–v3 and headerless behavior intact. +Classified preparation refusals retain code/status and bounded request-only +provenance; unknown material/execution hashes stay null and later checks deferred. +AgentClient, the CLI and WorkflowClient preserve/validate the selected report +without a second preparation or submission. + +The direct write set is `pipeline.py`, `agent_api.py`, `app.py`, +`project_cli.py`, `tuldok_client.py`, focused preflight/snapshot tests, +their canonical operation/report/CLI/client contracts and this plan's scoped +acceptance evidence. No persistence/schema, resource policy, runtime, README, +publication-retention or scientific-criterion implementation changes are selected. +Core/Router Implementation, Verification, Commit, Contracts/Schemas and IPC/ +Generated Contract guidance apply; existing JSON Schema machinery is reused. +The standards-authoring skill is excluded for ordinary application edits. + +Initial unit/transport and actual PostgreSQL/HTTP refusal witnesses passed at +their recorded source. Independent review then required exact owner/phase/check +layout validation and refusal of an explicitly null report; both are implemented +with adversarial tests. An earlier broader run retained two failures: an explicit +default argument changed a v3 private mock expectation, and a concurrent source +edit correctly tripped the imported-host identity fence. The old call is restored; +the fence is unchanged. Final acceptance requires a fresh source-frozen run and +independent review, recorded in the scoped report rather than reinterpreting that +earlier run as passing. + +The frozen local suite subsequently passed 65 tests in 200.178 seconds, zero +skips/failures/errors. All 126 existing operation hashes and the complete legacy +projection match 360bbda; only graph v4 is added. Actual HTTP/CLI refusal and inert +successful preparation keep their exact execution/authority boundaries. Three +exact owned schemas were removed. Source hashes, prior failures, test log and +independent final disposition are retained in +[the scoped acceptance report](reports/preflight-refusal-reports-20261020/acceptance.md). + +This completes neither the separate retained-recipe/Study preparation report nor +all C1/T1/M1. PUB-1's original private failure and SCI-1's actual scientific pair/ +criterion/environment inputs remain missing. Other product gaps remain separate. + ## 2026-10-19 — Exact recovered evidence and comparison - Continued from reviewed PR97 `0da7a81`; preserved independent synthetic witness diff --git a/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/acceptance.md b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/acceptance.md new file mode 100644 index 00000000..546f567e --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/acceptance.md @@ -0,0 +1,41 @@ +# Local graph preflight refusal qualification + +Graph preflight now returns an inspectable structured refusal to negotiated v4 +consumers while retaining its original HTTP code/status/message. Pipeline extends +its existing report owner with version 2 request-only failure reports. Refused +material/execution hashes are null; no partial proof is presented as checked. +Invalid, unsupported and blocked outcomes remain distinct from deferred unknown +checks. Bare early authorization/contract errors remain bare. + +The consumer validates all eleven required refusal checks, owner/phase/boundary +provenance, bounded details, request/catalog identity, report hash and original +typed outcome. Rehashed forged provenance, omitted checks, unexpected proof +hashes, null reports and credential-bearing reports refuse. CLI stderr and +WorkflowClient typed errors retain the selected report without retry or submit. + +The source-frozen local suite passed **65 tests in 200.178s, zero skips/failures/ +errors**. It includes actual PostgreSQL/HTTP/CLI refusal, old 1/2/3/headerless +projections, real authentication refusal, no process/grant/reservation effects, +successful inert captured preparation and finite Study regression, existing CLI +neutral execution/download, and focused client/report/storage fault cases. +All 126 pre-existing operation contract hashes and the complete legacy projection +match baseline 360bbda exactly; graph v4 is the only added contract. + +The positive static capture fixture explicitly uses `/usr/bin/true` and dummy +Torch/NumPy 0.0 files. It proves static ownership/contracts, not runnable numerical +training or inference. The neutral CLI recipe is not a lab workload. Three exact +owned fresh schemas were removed after fixture closure; unrelated schemas, +branches/worktrees and historical failures were preserved. + +[qualification.json](qualification.json) binds the eight changed Python/test +file hashes, final [test log](focused.log), environment, compatibility comparison +and earlier attempts. Initial failures remain retained: the default interpreter +lacked psycopg; an earlier 65-case run retained a v3 private-call expectation +failure and a correctly enforced host-source-drift refusal during an in-flight +edit. The v3 call was restored and the final run used unchanged source. Final +independent disposition is retained separately in `independent-review.json`. + +This is local implementation/acceptance for the graph structured-refusal slice. +No hosted CI, publication or merge is claimed. It does not close the separate +retained-recipe/Study report, all C1/T1/M1, publication retention/abandonment or +scientific criterion work. PUB-1 and SCI-1 still need their exact real-lab inputs. diff --git a/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/focused.log b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/focused.log new file mode 100644 index 00000000..e20defda --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/focused.log @@ -0,0 +1,78 @@ +test_actual_owner_refusal_and_old_http_contracts_leave_execution_counts_unchanged (test_run_preflight_refusals.PreflightRefusalHTTPTests.test_actual_owner_refusal_and_old_http_contracts_leave_execution_counts_unchanged) ... {"fixture_owner": "BoundExtensionRecipeTests", "new_schema": "catalog_766dc0e7726f41968e86349936aeacdb"} +ok +test_authorization_and_early_refusals_have_no_report_or_extra_graph_read (test_run_preflight_refusals.PreflightRefusalTests.test_authorization_and_early_refusals_have_no_report_or_extra_graph_read) ... ok +test_client_correlates_error_report_without_retry_and_wrappers_retain_it (test_run_preflight_refusals.PreflightRefusalTests.test_client_correlates_error_report_without_retry_and_wrappers_retain_it) ... ok +test_consumers_reject_forged_proof_status_duplicates_bounds_and_hash (test_run_preflight_refusals.PreflightRefusalTests.test_consumers_reject_forged_proof_status_duplicates_bounds_and_hash) ... ok +test_domain_preserves_original_error_and_does_not_reprepare_or_publish (test_run_preflight_refusals.PreflightRefusalTests.test_domain_preserves_original_error_and_does_not_reprepare_or_publish) ... ok +test_installed_contract_hashes_and_legacy_projection_are_unchanged (test_run_preflight_refusals.PreflightRefusalTests.test_installed_contract_hashes_and_legacy_projection_are_unchanged) ... ok +test_real_http_error_normalization_preserves_only_selected_report_and_redacts (test_run_preflight_refusals.PreflightRefusalTests.test_real_http_error_normalization_preserves_only_selected_report_and_redacts) ... ok +test_refusals_preserve_typed_outcome_and_only_request_provenance (test_run_preflight_refusals.PreflightRefusalTests.test_refusals_preserve_typed_outcome_and_only_request_provenance) ... ok +test_storage_owner_and_graph_drift_do_not_reuse_checked_material (test_run_preflight_refusals.PreflightRefusalTests.test_storage_owner_and_graph_drift_do_not_reuse_checked_material) ... ok +test_client_correlates_graph_revision_and_catalog_without_retry (test_run_preflight_report.PreflightReportTests.test_client_correlates_graph_revision_and_catalog_without_retry) ... ok +test_clone_or_revision_changes_report_without_claiming_new_execution (test_run_preflight_report.PreflightReportTests.test_clone_or_revision_changes_report_without_claiming_new_execution) ... ok +test_consumer_refuses_inconsistent_identities_even_with_repaired_report_hash (test_run_preflight_report.PreflightReportTests.test_consumer_refuses_inconsistent_identities_even_with_repaired_report_hash) ... ok +test_domain_calls_fresh_preparation_and_only_selected_v3_adds_report (test_run_preflight_report.PreflightReportTests.test_domain_calls_fresh_preparation_and_only_selected_v3_adds_report) ... ok +test_graph_drift_or_preparation_failure_cannot_emit_checked_report (test_run_preflight_report.PreflightReportTests.test_graph_drift_or_preparation_failure_cannot_emit_checked_report) ... ok +test_mutable_execution_facts_are_explicitly_deferred (test_run_preflight_report.PreflightReportTests.test_mutable_execution_facts_are_explicitly_deferred) ... ok +test_new_wire_contract_requires_closed_bounded_report_and_old_overlap_survives (test_run_preflight_report.PreflightReportTests.test_new_wire_contract_requires_closed_bounded_report_and_old_overlap_survives) ... ok +test_report_binds_exact_material_graph_and_its_own_content (test_run_preflight_report.PreflightReportTests.test_report_binds_exact_material_graph_and_its_own_content) ... ok +test_report_preserves_prepared_material_and_approval_identity (test_run_preflight_report.PreflightReportTests.test_report_preserves_prepared_material_and_approval_identity) ... ok +test_unsupported_report_versions_refuse_before_preparation (test_run_preflight_report.PreflightReportTests.test_unsupported_report_versions_refuse_before_preparation) ... ok +test_utf8_content_identity_is_distinct_from_ascii_wire_encoding (test_run_preflight_report.PreflightReportTests.test_utf8_content_identity_is_distinct_from_ascii_wire_encoding) ... ok +test_real_preparation_storage_refusal_has_no_admission_or_staging_effect (test_run_preflight_storage.PreflightStorageIntegrationTests.test_real_preparation_storage_refusal_has_no_admission_or_staging_effect) ... {"fixture_owner": "LargeRecipeContract", "new_schema": "catalog_538221870654414c9e98e28d3c93fabb"} +ok +test_admission_keeps_existing_exact_policy_and_boundary (test_run_preflight_storage.PreflightStorageTests.test_admission_keeps_existing_exact_policy_and_boundary) ... ok +test_generated_model_context_uses_existing_context_capture_limit (test_run_preflight_storage.PreflightStorageTests.test_generated_model_context_uses_existing_context_capture_limit) ... ok +test_preflight_includes_capture_that_admission_already_staged (test_run_preflight_storage.PreflightStorageTests.test_preflight_includes_capture_that_admission_already_staged) ... ok +test_report_labels_current_observation_without_claiming_a_reservation (test_run_preflight_storage.PreflightStorageTests.test_report_labels_current_observation_without_claiming_a_reservation) ... ok +test_selected_v3_checks_storage_before_issuing_report_and_rechecks_graph (test_run_preflight_storage.PreflightStorageTests.test_selected_v3_checks_storage_before_issuing_report_and_rechecks_graph) ... ok +test_storage_observation_cannot_hide_graph_drift_or_missing_owner (test_run_preflight_storage.PreflightStorageTests.test_storage_observation_cannot_hide_graph_drift_or_missing_owner) ... ok +test_storage_refusal_cannot_emit_report_or_change_old_projections (test_run_preflight_storage.PreflightStorageTests.test_storage_refusal_cannot_emit_report_or_change_old_projections) ... ok +test_unknown_storage_refuses_and_other_profiles_keep_existing_behavior (test_run_preflight_storage.PreflightStorageTests.test_unknown_storage_refuses_and_other_profiles_keep_existing_behavior) ... ok +test_exact_v2_reply_accepts_requested_mode_and_ordered_operands (test_workflow_client.ComparisonReplyTests.test_exact_v2_reply_accepts_requested_mode_and_ordered_operands) ... ok +test_exact_v2_reply_rejects_schema_valid_wrong_mode_sides_and_hashes (test_workflow_client.ComparisonReplyTests.test_exact_v2_reply_rejects_schema_valid_wrong_mode_sides_and_hashes) ... ok +test_legacy_v1_reply_keeps_its_existing_wire_contract (test_workflow_client.ComparisonReplyTests.test_legacy_v1_reply_keeps_its_existing_wire_contract) ... ok +test_5xx_and_negative_reconciliation_preserve_original_unknown_identity (test_workflow_client.WorkflowClientTests.test_5xx_and_negative_reconciliation_preserve_original_unknown_identity) ... ok +test_cancel_reports_request_without_claiming_drain (test_workflow_client.WorkflowClientTests.test_cancel_reports_request_without_claiming_drain) ... ok +test_cancelled_resume_never_completes_from_unvisited_prior_proofs (test_workflow_client.WorkflowClientTests.test_cancelled_resume_never_completes_from_unvisited_prior_proofs) ... ok +test_changed_inventory_local_success_and_closed_manifests_refuse (test_workflow_client.WorkflowClientTests.test_changed_inventory_local_success_and_closed_manifests_refuse) ... ok +test_exact_comparison_refuses_v1_without_implicit_operand_fallback (test_workflow_client.WorkflowClientTests.test_exact_comparison_refuses_v1_without_implicit_operand_fallback) ... ok +test_failure_preserves_success_and_outstanding_without_alternate_dispatch (test_workflow_client.WorkflowClientTests.test_failure_preserves_success_and_outstanding_without_alternate_dispatch) ... ok +test_hash_mismatch_and_no_clobber_race_keep_partial_typed_and_remove_provisional (test_workflow_client.WorkflowClientTests.test_hash_mismatch_and_no_clobber_race_keep_partial_typed_and_remove_provisional) ... ok +test_import_is_stdlib_only_with_startup_tripwires_in_clean_python (test_workflow_client.WorkflowClientTests.test_import_is_stdlib_only_with_startup_tripwires_in_clean_python) ... ok +test_interrupted_segments_resume_at_zero_with_whole_hash_and_current_authority (test_workflow_client.WorkflowClientTests.test_interrupted_segments_resume_at_zero_with_whole_hash_and_current_authority) ... ok +test_material_digest_uses_domain_utf8_canonical_json_for_unicode (test_workflow_client.WorkflowClientTests.test_material_digest_uses_domain_utf8_canonical_json_for_unicode) ... ok +test_missing_selected_submission_capability_refuses_without_dispatch (test_workflow_client.WorkflowClientTests.test_missing_selected_submission_capability_refuses_without_dispatch) ... ok +test_monitor_deadline_and_stop_never_call_cancel_and_restore_transport_budget (test_workflow_client.WorkflowClientTests.test_monitor_deadline_and_stop_never_call_cancel_and_restore_transport_budget) ... ok +test_pre_stopped_collection_never_bootstraps_and_retains_prior_item_proofs (test_workflow_client.WorkflowClientTests.test_pre_stopped_collection_never_bootstraps_and_retains_prior_item_proofs) ... ok +test_public_inventory_can_include_unselectable_manifest_metadata (test_workflow_client.WorkflowClientTests.test_public_inventory_can_include_unselectable_manifest_metadata) ... ok +test_read_budget_refuses_mutation_and_cleans_thread_local_context (test_workflow_client.WorkflowClientTests.test_read_budget_refuses_mutation_and_cleans_thread_local_context) ... ok +test_resume_rejects_destination_replacement_during_whole_hash_verification (test_workflow_client.WorkflowClientTests.test_resume_rejects_destination_replacement_during_whole_hash_verification) ... ok +test_study_acknowledgement_matches_owner_request_parent_catalog_and_grant (test_workflow_client.WorkflowClientTests.test_study_acknowledgement_matches_owner_request_parent_catalog_and_grant) ... ok +test_unknown_submission_never_retries_and_explicit_reconcile_keeps_identity (test_workflow_client.WorkflowClientTests.test_unknown_submission_never_retries_and_explicit_reconcile_keeps_identity) ... ok +test_authentication_and_invalid_capabilities_never_select_legacy_dispatch (test_agent_project_cli.CommandSelectionTests.test_authentication_and_invalid_capabilities_never_select_legacy_dispatch) ... ok +test_invalid_command_arguments_refuse_before_capability_bootstrap (test_agent_project_cli.CommandSelectionTests.test_invalid_command_arguments_refuse_before_capability_bootstrap) ... ok +test_open_and_inspect_select_snapshots_or_published_legacy_operations_before_dispatch (test_agent_project_cli.CommandSelectionTests.test_open_and_inspect_select_snapshots_or_published_legacy_operations_before_dispatch) ... ok +test_selected_command_failure_never_retries_the_legacy_operation (test_agent_project_cli.CommandSelectionTests.test_selected_command_failure_never_retries_the_legacy_operation) ... ok +test_missing_grant_invalid_selection_and_stale_source_leave_no_run (test_agent_project_cli.ProjectCLIHTTPTests.test_missing_grant_invalid_selection_and_stale_source_leave_no_run) ... ok +test_open_prepare_approve_run_and_download_share_real_saved_owners (test_agent_project_cli.ProjectCLIHTTPTests.test_open_prepare_approve_run_and_download_share_real_saved_owners) ... ok +test_argument_file_refuses_symlinks_duplicate_keys_nonfinite_and_fifo (test_agent_project_cli.PublicationTests.test_argument_file_refuses_symlinks_duplicate_keys_nonfinite_and_fifo) ... ok +test_concurrent_destination_creation_refuses_publication_without_overwriting (test_agent_project_cli.PublicationTests.test_concurrent_destination_creation_refuses_publication_without_overwriting) ... ok +test_corrupt_whole_bytes_and_refused_read_leave_no_published_or_partial_file (test_agent_project_cli.PublicationTests.test_corrupt_whole_bytes_and_refused_read_leave_no_published_or_partial_file) ... ok +test_existing_regular_and_dangling_symlink_destinations_are_preserved (test_agent_project_cli.PublicationTests.test_existing_regular_and_dangling_symlink_destinations_are_preserved) ... ok +test_valid_empty_and_nonempty_bytes_are_published_with_private_mode (test_agent_project_cli.PublicationTests.test_valid_empty_and_nonempty_bytes_are_published_with_private_mode) ... ok +test_all_published_main_contracts_keep_their_exact_identities_and_overlap (test_agent_project_snapshots.SnapshotRegistryTests.test_all_published_main_contracts_keep_their_exact_identities_and_overlap) ... ok +test_explicit_snapshot_contracts_preserve_the_installed_legacy_registry (test_agent_project_snapshots.SnapshotRegistryTests.test_explicit_snapshot_contracts_preserve_the_installed_legacy_registry) ... ok +test_snapshot_arguments_are_closed_and_bounded (test_agent_project_snapshots.SnapshotRegistryTests.test_snapshot_arguments_are_closed_and_bounded) ... ok +test_agent_graph_preflight_and_trial_preparation_are_inert_under_owner_budget (test_agent_project_snapshots.AgentSnapshotHTTPTests.test_agent_graph_preflight_and_trial_preparation_are_inert_under_owner_budget) +Tiny non-runnable runtime bytes prove static capture, never CPU execution. ... {"fixture_owner": "BoundExtensionRecipeTests", "new_schema": "catalog_2759b4b3693c4a8c8d3667c9c134cc6b"} +ok + +---------------------------------------------------------------------- +Ran 65 tests in 200.178s + +OK +{"removed_exact_owned_schema": "catalog_2759b4b3693c4a8c8d3667c9c134cc6b"} +{"removed_exact_owned_schema": "catalog_538221870654414c9e98e28d3c93fabb"} +{"removed_exact_owned_schema": "catalog_766dc0e7726f41968e86349936aeacdb"} +{"started_utc": "2026-10-20T17:49:26.382654+00:00", "completed_utc": "2026-10-20T17:52:46.672172+00:00", "tests": 65, "failures": 0, "errors": 0, "skipped": 0, "owned_schemas_removed": ["catalog_2759b4b3693c4a8c8d3667c9c134cc6b", "catalog_538221870654414c9e98e28d3c93fabb", "catalog_766dc0e7726f41968e86349936aeacdb"], "no_constructor_or_domain_substitution": "Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.", "source_tree": "uncommitted isolated branch from360bbda; exactsourcehashmanifest retained separately"} diff --git a/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/independent-review.json b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/independent-review.json new file mode 100644 index 00000000..e488e2fc --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/independent-review.json @@ -0,0 +1,208 @@ +{ + "authored_acceptance_review": { + "disposition": "CLEAR scopedsource/evidenceclaimaccuracy", + "durability_and_sideeffect_limit": "Localcommitpreparation only. Detailed126comparisonandpriorfailurelogs are /tmppath/hashboundbyqualification; no remotearchive/publication/durabilityclaim. Rootcopyofthisreceipt as independent-review.json is permitted by delegatedlocalcheckpointtask; receiptneverhashesitself.", + "files": [ + { + "bytes": 2700, + "path": "/workspace/Tuldok-preflight-refusals-20261020/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/acceptance.md", + "sha256": "b1f6ff3ad905705c42b6fd7cf8ecbcae7d51f99ae1392e615fe8feb728bf197b" + }, + { + "bytes": 4280, + "path": "/workspace/Tuldok-preflight-refusals-20261020/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/qualification.json", + "sha256": "c6a0bdc322553ba76fba1d5138cca01957ab35f4c23d36ede6c6e3e0bcabb8c3" + }, + { + "bytes": 13797, + "path": "/workspace/Tuldok-preflight-refusals-20261020/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/focused.log", + "sha256": "6f88fcb9089dadc04048078671aa17a3c5067724256533aa06f34e6dd449dc0c" + } + ], + "history": "Initialmissingpsycopgimporterror and earlier65case2failures retained with exacthashes. Source-driftguardpreserved; no retroactivepassattribution.", + "remaining_product": "Graphonlyclassification; Study/materialreport/allC1T1M1/C2retention/T7criterion/PUB1SCI1explicitlyopen.", + "scope_and_count": "65/200.178s plainOK zero skip/fail/error at frozen8Python/testhashes. No per-boundary hiddenrealTorch claim; staticpositiveprofile explicitlytiny/usr/bin/true/dummypackages andCLIexecutionneutral. All126rootexecutedpriorcontracts+legacycomparison exact; currentreviewread/bound butdidnotrerun." + }, + "authorization_limit": "Rootowns coherentlocalcommit. This reviewauthorizes no publication/push/PR/merge/retarget/cleanup/credentialchange or additionalexecution.", + "baseline": "360bbda950cc48ed3c3d9193539039b68e0ac8cb", + "blocking_findings": [], + "branch": "feature/preflight-refusal-reports-20261020", + "correctness_review": { + "consumer_transport": "V4-only reportfield retained bycanonicalHTTPerrornormalizer,thenclosedschema/hash/requestcataloggraphrevision/statuscode/aggregatecheck/evidence/provenancevalidated. Missingreportvalidbare;presentnull/malformedrejected. CLIstderrJSON/nonzeroandWorkflowtypederrorpreservevalidatedreportwithoutnewmanifest/retry; MCPunchangedstructuredtransportnotnewactualMCPqualificationhere.", + "error_privacy": "Constantboundedreportdetails omitexceptiontext/path/traceback. OriginaltypedWorkbenchErrorcode/status/message preservedoutside report. Selectedconsumerredactsowncredentialfromtoplevelerror and rejectscredentialinreportwithout echo. Auth/early/unclassified/nonWorkbencherrorsstaybare, with noextra preparation fordecoration.", + "failure_proof": "Requestcatalog/graph/requestedrevision only, execution/materialSHA null on allclassifiedfailuresincluding postpreparestorage/drift. No checkedfailurematerial, no emptyhash/zeroSHA, no parsedexceptionfamily. GenericPipelinepreparation or explicitlyenteredRunsstorage only.", + "ownership": "Pipelineprepare/report remainsoneowner; Runsstorage invoked through existingcheck. No resourcepolicy/runtime/custody/accounting/SQL/staticUI changes.", + "unknown_boundaries": "READYGO/authorityadmission/resourceavailability/custody/outputpublication/science deferredorunobserved; reportdoesnotreserveoradmit.", + "versioning": "Reportv2+operationv4 copiedfromoldschemas; preservedv3privatehelpercallshape andsuccessprojection; headerless/v1v2oldsuccess andv1v2v3olderrorsqualified." + }, + "disposition": "CLEAR exact current local graphv4 structured-refusal implementation, all13source/test/canonicaldocfiles plus authoredacceptance/qualification/focusedlog. Root may bind coherentlocalcommit; no publication/hostedCI/merge/fullC1acceptance implied.", + "documentation_review": "Canonicalreport/agent/CLI/clientcontracts andupdatedledger matchproducer+consumerbehavior,classifiedallowlist,bareearlyerrors and graph-onlypartialacceptance. Newversion2report andv4selectedoperationavoidalteringoldcontracts. Authoredacceptance describesactualrun/testfixturefidelityandpriorfailuresaccurately.", + "evidence": [ + { + "bytes": 628, + "path": "/tmp/tuldok-preflight-refusals-20261020/focused-result.json", + "sha256": "f1604162b02c08add94724ab817bb1114d3682a131d5eb6eef2fd12dc22ff959" + }, + { + "bytes": 13797, + "path": "/tmp/tuldok-preflight-refusals-20261020/focused-final.log", + "sha256": "6f88fcb9089dadc04048078671aa17a3c5067724256533aa06f34e6dd449dc0c" + }, + { + "bytes": 32969, + "path": "/tmp/tuldok-preflight-refusals-20261020/all-prior-contracts.json", + "sha256": "78e6fa9dfdc344ec48f31534bc9fd5ad8c1909bd9d87c4a75e2717c48d5da539" + }, + { + "bytes": 761, + "path": "/tmp/tuldok-preflight-refusals-20261020/source-for-final-tests.json", + "sha256": "b1194d85bdc03bfd58a8b924eea99f42edad80dcbf7b111fcaff0ba4e1c5b749" + }, + { + "bytes": 402, + "path": "/tmp/tuldok-preflight-refusals-20261020/actual-owner-refusal-initial.log", + "sha256": "a35dcdd184cc6f83a1f23b1870cedfebd025a6b9058a020d3c9bf381f8d284a9" + }, + { + "bytes": 21913, + "path": "/tmp/tuldok-preflight-refusals-20261020/focused-first.log", + "sha256": "c7cf18350d63dffa0e36ec625218d89cea884bcede56bc4844d506d99264917f" + }, + { + "bytes": 628, + "path": "/tmp/tuldok-preflight-refusals-20261020/focused-first-result.json", + "sha256": "01cd5f8171d2ac8b84bf5276792c4dc02518a7d5071759f0a09cf51e6f6f6c2d" + }, + { + "bytes": 1225, + "path": "/tmp/tuldok-preflight-refusals-20261020/storage-unit.log", + "sha256": "c780943574c1898427f54f3505854319689b9fd61a6af8738b9b87f4a09451d8" + }, + { + "bytes": 2733, + "path": "/tmp/tuldok-preflight-refusals-20261020/run-focused.py", + "sha256": "dedff2b76fd7ede4b930d9da853bb60c6e01091460ba3b14998c98369189e046" + }, + { + "bytes": 2700, + "path": "/workspace/Tuldok-preflight-refusals-20261020/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/acceptance.md", + "sha256": "b1f6ff3ad905705c42b6fd7cf8ecbcae7d51f99ae1392e615fe8feb728bf197b" + }, + { + "bytes": 4280, + "path": "/workspace/Tuldok-preflight-refusals-20261020/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/qualification.json", + "sha256": "c6a0bdc322553ba76fba1d5138cca01957ab35f4c23d36ede6c6e3e0bcabb8c3" + }, + { + "bytes": 13797, + "path": "/workspace/Tuldok-preflight-refusals-20261020/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/focused.log", + "sha256": "6f88fcb9089dadc04048078671aa17a3c5067724256533aa06f34e6dd449dc0c" + }, + { + "bytes": 10215, + "path": "/tmp/tuldok-preflight-refusal-independent-review.json", + "sha256": "15e1d3624271efb54f30dd2d78b02974314c7c75eb9dbeffa2d92543c258336b" + } + ], + "fingerprint_encoding": "SHA256 UTF8 json.dumps(path_to_sha256,sort_keys=True,separators=(comma,colon)); paths+digestvaluesASCII.", + "history_and_limitations": { + "initial_actual_refusal": "1passed12.843s atrecordedearliersource; final65includesexpandedactualCLI/headerless/authcase atfrozennewsource.", + "preserved_failed_runs": [ + "storage-unit.log: initialdefaultpythonimportfailedmissingpsycopg, notproductfailure", + "focused-first.log:65tests170.736s/2failures, v3helperMockcallshape and importedhostsourcechangedinflight; restoredcallshapeandfreshfrozenrun, nohostguardweakening" + ], + "remaining_scope": "Separate retained-recipe/Study preparationreport and fullC1/T1/M1 stillopen; C2retention and M6criterion tasks/PUB1SCI1 unchanged. No fullrepo/hostedCI/browser/science/CUDAclaim." + }, + "qualification": { + "actual_scope": "RealdisposablePostgreSQL/productionHTTP/AgentClienttypedrefusal, actualCLIinprocessstderr, oldv1\u2013v3+headerless/auth,noPopenandunchanged10domain/admissiontables. Existingstaticpositivefixture usesrealownerswithtiny/usr/bin/true+dummyTorch/NumPy0.0snapshot; no runnableTorch/inference/numericalclaim.", + "cleanup": "Finalrunnerrecords3exactfreshschemasremovedwithSQLabsenceassertions; revieweddriverandcompleteoutput. ReviewerperformednoindependentPGqueries/deletions. Driverrecordsafterunchangedfixtureconstruction, so noclaimconstructorfailurepreregistration/crashdurability.", + "compatibility": { + "all126_prior_contract_hashes_exact": true, + "baseline_module_gitblob_and_saved_file_hashmatch": true, + "current_module_hashmatch": true, + "fidelity": "Rootperformedbaseline/currentmodulecontractcomputation;reviewerindependentlychecked126uniqueequalpairsandbothmodulehashbindingswithoutimporting/executingmodules.", + "legacy_projection_exact": true, + "only_added": "graphs.preflight_external/4", + "receipt": { + "bytes": 32969, + "path": "/tmp/tuldok-preflight-refusals-20261020/all-prior-contracts.json", + "sha256": "78e6fa9dfdc344ec48f31534bc9fd5ad8c1909bd9d87c4a75e2717c48d5da539" + } + }, + "executor": "root; reviewer didnotreruntests", + "final_focused": { + "errors": 0, + "failures": 0, + "frozen_source_manifest": { + "bytes": 761, + "path": "/tmp/tuldok-preflight-refusals-20261020/source-for-final-tests.json", + "sha256": "b1194d85bdc03bfd58a8b924eea99f42edad80dcbf7b111fcaff0ba4e1c5b749" + }, + "log": { + "bytes": 13797, + "path": "/tmp/tuldok-preflight-refusals-20261020/focused-final.log", + "sha256": "6f88fcb9089dadc04048078671aa17a3c5067724256533aa06f34e6dd449dc0c" + }, + "ran": 65, + "result": { + "completed_utc": "2026-10-20T17:52:46.672172+00:00", + "errors": 0, + "failures": 0, + "no_constructor_or_domain_substitution": "Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.", + "owned_schemas_removed": [ + "catalog_2759b4b3693c4a8c8d3667c9c134cc6b", + "catalog_538221870654414c9e98e28d3c93fabb", + "catalog_766dc0e7726f41968e86349936aeacdb" + ], + "skipped": 0, + "source_tree": "uncommitted isolated branch from360bbda; exactsourcehashmanifest retained separately", + "started_utc": "2026-10-20T17:49:26.382654+00:00", + "tests": 65 + }, + "scope_source_sha256": { + "agent_api.py": "f2a4322616fce373b3e04e09cd516676df9cfbd5dd8f4202ba8bb1498cf2f5af", + "app.py": "4a5756538408f6124bae6722f8477e3814c4a972d61f22b9bfbd085d4fa0ad4a", + "pipeline.py": "5806c31a9be9ab37a0325ad1fe29c64bd9bbd1d546af3b384392b6d5a6a9704b", + "project_cli.py": "16f9da9c2d7d180a0bb43e403d4088c9bea73f7d175170f9e816157cb510f88d", + "tests/test_agent_project_snapshots.py": "20630275fa467210ffbd37492e32a4243907a04d749dd20193632abb33984779", + "tests/test_run_preflight_refusals.py": "78d6e6771e03951c17b7d8c12a8cd365f79f738283e31ab9435a1dc3476e842e", + "tests/test_run_preflight_report.py": "3c626b713154472023acf13c6e208e3483f94dfd957a3a777f456d87cac4e767", + "tuldok_client.py": "3cdfb0a9823ec7527833a8838911c3a1d62ecc0d8376c438e3100c6c9f30aee6" + }, + "seconds": 200.178, + "skipped": 0 + } + }, + "resolved_review_findings": [ + { + "id": "C1-R1", + "initial_finding": "Rehashedschema-validwrongowner/phase/code/boundary ormissingdeferredrowscouldpass v4refusalconsumer.", + "resolution": "Exact11-code layout andfixedprovenance mapping, exactlyoneallowedfailedboundaryandallotherrowsdeferred; rehashedadversarialnegatives added. Explanatorydetail remains boundedpresentationtext, neverauthority." + }, + { + "id": "C1-R2", + "initial_finding": "PresentnullreportwasdroppedasifabsentbyHTTPnormalization.", + "resolution": "Presence trackedseparately; presentnullpassesinto selectedclosedvalidator andfails; actualcontrolledHTTPnegativeadded." + } + ], + "reviewed13_file_fingerprint": "97b7c0fa84a9c87751a130791091d81fd1154cd91e96fcf27d34f9ea7d752bd4", + "reviewed_files": { + "agent_api.py": "f2a4322616fce373b3e04e09cd516676df9cfbd5dd8f4202ba8bb1498cf2f5af", + "app.py": "4a5756538408f6124bae6722f8477e3814c4a972d61f22b9bfbd085d4fa0ad4a", + "docs/contracts/agent-operations.md": "bbc15d4db47f027db35d8c2580db97139adb781049d7ad58e022bcf94eb9642e", + "docs/contracts/project-cli.md": "0e39f1d702f301d44097234fee645851fd19e6906a287f418f8769ed8bbbb92b", + "docs/contracts/run-preflight-publication.md": "419d7c50e5cdfbdbfae1d336867f5ae6aec8b3ce0213896c174add0d89c5cc3d", + "docs/contracts/workflow-client.md": "491582d709a5e1776c780f75ebada25b8e57b40eedad177473686b2de4f798be", + "docs/plans/lab-workflow-reliability/execution-ledger.md": "adf961ebde7620d40fc54b21f61d8b8d2c4f158998d3fa2fa087ac7e0f0409de", + "pipeline.py": "5806c31a9be9ab37a0325ad1fe29c64bd9bbd1d546af3b384392b6d5a6a9704b", + "project_cli.py": "16f9da9c2d7d180a0bb43e403d4088c9bea73f7d175170f9e816157cb510f88d", + "tests/test_agent_project_snapshots.py": "20630275fa467210ffbd37492e32a4243907a04d749dd20193632abb33984779", + "tests/test_run_preflight_refusals.py": "78d6e6771e03951c17b7d8c12a8cd365f79f738283e31ab9435a1dc3476e842e", + "tests/test_run_preflight_report.py": "3c626b713154472023acf13c6e208e3483f94dfd957a3a777f456d87cac4e767", + "tuldok_client.py": "3cdfb0a9823ec7527833a8838911c3a1d62ecc0d8376c438e3100c6c9f30aee6" + }, + "reviewed_utc": "2026-10-20T17:56:59.039406+00:00", + "scope": "Independent READONLY review; no sourcewrites/tests/runtime/PGquery/remote/ref/CIoperations. Localreviewreceipt only. Review includes untrackednewtestsfile absentfrom initialgitdiff.", + "source_state": "uncommitted reviewedbytes; final rootcommitbinding mayfollowwithoutsourcechange", + "worktree": "/workspace/Tuldok-preflight-refusals-20261020" +} diff --git a/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/qualification.json b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/qualification.json new file mode 100644 index 00000000..75324eff --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/preflight-refusal-reports-20261020/qualification.json @@ -0,0 +1,75 @@ +{ + "format": "preflight_refusal_local_qualification_v1", + "plan": "docs/plans/lab-workflow-reliability/plan.md", + "scope": "C1/T1 graph structuredrefusal slice only; retained-recipe/Study and overallC1/T1 remainopen", + "base_commit": "360bbda950cc48ed3c3d9193539039b68e0ac8cb", + "branch": "feature/preflight-refusal-reports-20261020", + "source_sha256": { + "pipeline.py": "5806c31a9be9ab37a0325ad1fe29c64bd9bbd1d546af3b384392b6d5a6a9704b", + "agent_api.py": "f2a4322616fce373b3e04e09cd516676df9cfbd5dd8f4202ba8bb1498cf2f5af", + "app.py": "4a5756538408f6124bae6722f8477e3814c4a972d61f22b9bfbd085d4fa0ad4a", + "project_cli.py": "16f9da9c2d7d180a0bb43e403d4088c9bea73f7d175170f9e816157cb510f88d", + "tuldok_client.py": "3cdfb0a9823ec7527833a8838911c3a1d62ecc0d8376c438e3100c6c9f30aee6", + "tests/test_run_preflight_report.py": "3c626b713154472023acf13c6e208e3483f94dfd957a3a777f456d87cac4e767", + "tests/test_run_preflight_refusals.py": "78d6e6771e03951c17b7d8c12a8cd365f79f738283e31ab9435a1dc3476e842e", + "tests/test_agent_project_snapshots.py": "20630275fa467210ffbd37492e32a4243907a04d749dd20193632abb33984779" + }, + "focused": { + "started_utc": "2026-10-20T17:49:26.382654+00:00", + "completed_utc": "2026-10-20T17:52:46.672172+00:00", + "tests": 65, + "failures": 0, + "errors": 0, + "skipped": 0, + "owned_schemas_removed": [ + "catalog_2759b4b3693c4a8c8d3667c9c134cc6b", + "catalog_538221870654414c9e98e28d3c93fabb", + "catalog_766dc0e7726f41968e86349936aeacdb" + ], + "no_constructor_or_domain_substitution": "Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.", + "source_tree": "uncommitted isolated branch from360bbda; exactsourcehashmanifest retained separately" + }, + "focused_log_sha256": "6f88fcb9089dadc04048078671aa17a3c5067724256533aa06f34e6dd449dc0c", + "prior_contracts": { + "count": 126, + "all_sha256_exact": true, + "legacy_projection_exact": true, + "only_added_contract": { + "operation": "graphs.preflight_external", + "version": 4 + }, + "full_comparison_local_path": "/tmp/tuldok-preflight-refusals-20261020/all-prior-contracts.json", + "full_comparison_sha256": "78e6fa9dfdc344ec48f31534bc9fd5ad8c1909bd9d87c4a75e2717c48d5da539" + }, + "prior_attempts": [ + { + "path": "/tmp/tuldok-preflight-refusals-20261020/initial-focused.log", + "sha256": "1151e8858de3961a180e8db4e5fd6671ec77503069cbe00f81430255d0b19c1d", + "state": "8passes/1DBskip; initialsource" + }, + { + "path": "/tmp/tuldok-preflight-refusals-20261020/actual-owner-refusal-initial.log", + "sha256": "a35dcdd184cc6f83a1f23b1870cedfebd025a6b9058a020d3c9bf381f8d284a9", + "state": "1actualPGHTTPpass12.843s; exactownschema removed" + }, + { + "path": "/tmp/tuldok-preflight-refusals-20261020/storage-unit.log", + "sha256": "c780943574c1898427f54f3505854319689b9fd61a6af8738b9b87f4a09451d8", + "state": "DefaultPython lackedpsycopg; moduleimporterror, notapass" + }, + { + "path": "/tmp/tuldok-preflight-refusals-20261020/focused-first.log", + "sha256": "c7cf18350d63dffa0e36ec625218d89cea884bcede56bc4844d506d99264917f", + "state": "65tests170.736s2fail: oldv3privatecall restored; in-flight sourcechange correctly fenced; supersededonlybyfreshsource-frozenrun" + } + ], + "independent_review": "independent-review.json", + "environment": "Existing localPython3.12runtime with provisioned base dependencies and PostgreSQL17Unixsocket. No dependency installation or hostcredential changes. Test-only authfixtures and neutralrecipe execution are notlab science/training evidence.", + "limits": [ + "Localfocusedqualification only; no hostedCI/publication/merge", + "Static positive runtime uses /usr/bin/true and dummy torch/numpy0.0 as inert capturefixture; no Torch/inference qualification", + "PUB-1originalprivatefailure andSCI-1actualscientificpair/criterion/hardware remainmissing", + "Publication retention/abandonment andscientificcriterion code remain separate" + ], + "claim": "Boundedclassifiedgraphrefusal reporting, exactlegacycompatibility, safeconsumerdelivery/correlation, inertrefusal effects, and existingCLI/staticpreparation regressions only." +} diff --git a/pipeline.py b/pipeline.py index 05a59a98..b970300b 100644 --- a/pipeline.py +++ b/pipeline.py @@ -42,7 +42,7 @@ def digest(value): return hashlib.sha256(encode(value).encode()).hexdigest() -def _external_preflight_report(graph, prepared, *, storage=None): +def _external_preflight_report(graph, prepared, *, storage=None, version=1): """Project fresh prepare() proofs; this is neither admission nor a retained proof cache.""" material = prepared['material'] runtime = material.get('torch_runtime', material.get('cpu_snapshot', material['runtime'])) @@ -86,6 +86,42 @@ def check(code, owner, phase, state, evidence, boundary, detail): 'catalog_id': material['catalog_id'], 'graph_id': graph['id'], 'graph_revision': graph['revision'], 'execution_sha256': prepared['execution_sha256'], 'material_sha256': prepared['material_sha256'], 'checks': checks} + if version == 2: + report.update(format='external_preflight_report_v2', refusal=None) + return dict(report, sha256=digest(report)) + + +def _external_preflight_refusal(catalog_id, graph_id, revision, error, *, storage=False): + """Request provenance only: a refused preparation establishes no material proof.""" + states = {'invalid': 'invalid', 'unsupported': 'unsupported', 'unavailable': 'blocked', + 'stale': 'blocked', 'conflict': 'blocked', 'too_large': 'blocked', 'storage': 'blocked'} + if error.code not in states or not 400 <= error.status <= 599 or error.status in (401, 403): + return None + state = states[error.code] + report = {'format': 'external_preflight_report_v2', 'state': state, 'admission': 'not_reserved', + 'catalog_id': catalog_id, 'graph_id': graph_id, 'graph_revision': revision, + 'execution_sha256': None, 'material_sha256': None, + 'refusal': {'code': error.code, 'status': error.status}} + evidence = digest(report) + report['checks'] = [{'code': 'storage_capacity' if storage else 'material_preparation', + 'owner': 'runs' if storage else 'pipeline', 'phase': 'preparation', 'state': state, + 'evidence_sha256': evidence, + 'boundary': 'Runs current storage observation' if storage else 'Pipeline fresh material preparation', + 'detail': 'Preparation refused with the original typed outcome. This reference binds the request and refusal, not verified material.'}] + for code, owner, phase, boundary in ( + ('source_inputs', 'pipeline', 'preparation', 'Successful fresh source and input capture'), + ('runtime_inventory', 'pipeline', 'preparation', 'Successful fresh runtime inventory verification'), + ('host_closure', 'pipeline', 'preparation', 'Successful fresh host identity verification'), + ('output_contract', 'pipeline', 'preparation', 'Successful fresh output and evidence validation'), + ('capture_authority', 'pipeline', 'preparation', 'Current source and input disclosure owners'), + ('authority_admission', 'runs', 'admission', 'Runs atomic admission'), + ('resource_availability', 'runs', 'admission', 'Runs current resource admission'), + ('runtime_ready_go', 'runs', 'dispatch', 'Runs runtime READY/GO'), + ('custody_completion', 'runs', 'execution', 'Runs custody and terminal drainage'), + ('output_publication', 'runs', 'publication', 'Runs durable output publication')): + report['checks'].append({'code': code, 'owner': owner, 'phase': phase, 'state': 'deferred', + 'evidence_sha256': evidence, 'boundary': boundary, + 'detail': 'Unknown in this refused preparation; no checked material, execution, device or scientific acceptance is claimed.'}) return dict(report, sha256=digest(report)) @@ -351,26 +387,41 @@ def preparation_timings(self, graph_id, revision): @authorized('inspect', 'pipeline.preflight_external') def preflight_external(self, graph_id, revision, *, contract_version=None): - if contract_version is not None and (type(contract_version) is not int or contract_version not in (1, 2, 3)): + if contract_version is not None and (type(contract_version) is not int or contract_version not in (1, 2, 3, 4)): raise WorkbenchError('Unsupported external preflight version.', 'unsupported', 409) + if contract_version == 4: + identifier(graph_id, 'Graph ID') + if type(revision) is not int or revision < 1: + raise WorkbenchError('Invalid graph revision.') timing = Pipelines._preparation_clock(self, graph_id, revision, 'graphs.preflight_external') with phase_timings.observe(timing, 'prepare', 'preparing_material'): - graph = self.get(graph_id) - if graph.get('recipe') != 'external_recipe' or type(revision) is not int or graph['revision'] != revision: - raise WorkbenchError('External graph changed or is unavailable.', 'stale', 409) - result, _captured = recipe_contract.prepare(self.dataset.projects, graph, execution_digest(graph), + storage_boundary = False + try: + graph = self.get(graph_id) + if graph.get('recipe') != 'external_recipe' or type(revision) is not int or graph['revision'] != revision: + raise WorkbenchError('External graph changed or is unavailable.', 'stale', 409) + result, _captured = recipe_contract.prepare(self.dataset.projects, graph, execution_digest(graph), getattr(self.dataset, 'external_runtime_profile', None), releases=self.dataset.releases, runs=getattr(self.dataset, 'runs', None)) - timing.material_sha256 = result['material_sha256'] - storage = None - if contract_version == 3 and result['material']['resources'].get('profile') == recipe_contract.LARGE_OUTPUT_PROFILE: - runs = getattr(self.dataset, 'runs', None) - if runs is None: - raise WorkbenchError('Execution storage owner is unavailable.', 'unavailable', 409) - storage = runs._check_external_storage(result['material'], pending_capture=True) - if self.get(graph_id) != graph: - raise WorkbenchError('Graph changed during preflight.', 'stale', 409) + timing.material_sha256 = result['material_sha256'] + storage = None + if contract_version in (3, 4) and result['material']['resources'].get('profile') == recipe_contract.LARGE_OUTPUT_PROFILE: + storage_boundary = True + runs = getattr(self.dataset, 'runs', None) + if runs is None: + raise WorkbenchError('Execution storage owner is unavailable.', 'unavailable', 409) + storage = runs._check_external_storage(result['material'], pending_capture=True) + storage_boundary = False + if self.get(graph_id) != graph: + raise WorkbenchError('Graph changed during preflight.', 'stale', 409) + except WorkbenchError as error: + if contract_version == 4: + error.preflight_report = _external_preflight_refusal( + self.db.dataset_id, graph_id, revision, error, storage=storage_boundary) + raise if contract_version == 3: result = dict(result, preflight_report=_external_preflight_report(graph, result, storage=storage)) + elif contract_version == 4: + result = dict(result, preflight_report=_external_preflight_report(graph, result, storage=storage, version=2)) return result @authorized('inspect', 'pipeline.capture_external') diff --git a/project_cli.py b/project_cli.py index 27155d4b..9af517d0 100644 --- a/project_cli.py +++ b/project_cli.py @@ -42,7 +42,8 @@ def invoke(client, operation, arguments, *, observer=None, observation=None): if observer is not None else client.call(operation, arguments)) if envelope['status'] >= 400: error = envelope['result'] - raise AgentAPIError(error['error'], error['code'], envelope['status']) + raise AgentAPIError(error['error'], error['code'], envelope['status'], + preflight_report=error.get('preflight_report')) return envelope @@ -324,7 +325,10 @@ def main(argv=None, *, observer=None): http_status = status return 0 if _write_output(sys.stdout, result, observer, observation, status=status, http_status=http_status) else 1 except AgentAPIError as error: - _write_output(sys.stderr, {'code': error.code, 'error': str(error), 'status': error.status}, + result = {'code': error.code, 'error': str(error), 'status': error.status} + if error.preflight_report is not None: + result['preflight_report'] = error.preflight_report + _write_output(sys.stderr, result, observer, observation, status=error.status, failed=True, observed_outcome=( 'transport-error' if error.code in ('transport', 'response_too_large') else 'registry-refusal' if error.code == 'registry_mismatch' else diff --git a/tests/test_agent_project_snapshots.py b/tests/test_agent_project_snapshots.py index 900efba8..0af08f22 100644 --- a/tests/test_agent_project_snapshots.py +++ b/tests/test_agent_project_snapshots.py @@ -54,7 +54,7 @@ def test_explicit_snapshot_contracts_preserve_the_installed_legacy_registry(self for name in ('list', 'get', 'open'): self.assertEqual(selected['project_snapshots.' + name], 1 if name == 'list' else 2) for name, version in (('recipes.inspect', 4), ('graphs.create_external', 2), - ('graphs.preflight_external', 3), ('models.publish', 2), + ('graphs.preflight_external', 4), ('models.publish', 2), ('studies.prepare_trial', 2)): self.assertEqual(selected[name], version) diff --git a/tests/test_run_preflight_refusals.py b/tests/test_run_preflight_refusals.py new file mode 100644 index 00000000..fa6e88f9 --- /dev/null +++ b/tests/test_run_preflight_refusals.py @@ -0,0 +1,225 @@ +"""Versioned refusal semantics and real transport; no numerical/runtime acceptance.""" +import copy +from contextlib import contextmanager, redirect_stderr +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +import io +import json +import os +import threading +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch + +import access +import agent_api as api +import pipeline +import project_cli +import test_run_preflight_report as prior +from tuldok_client import WorkflowClient +from workbench import WorkbenchError + + +class PreflightRefusalTests(unittest.TestCase): + def setUp(self): + self.fixture = prior.PreflightReportTests('runTest') + self.fixture.setUp() + self.arguments = {'graph_id': 'a' * 32, 'revision': 2} + + def refusal(self, code='unavailable', status=409): + error = WorkbenchError('Private producer detail must not enter report', code, status) + report = pipeline._external_preflight_refusal('b' * 32, 'a' * 32, 2, error) + return {'status': status, 'result': {'code': code, 'error': str(error), 'preflight_report': report}} + + @staticmethod + def rehash(report): + report['sha256'] = pipeline.digest({k: v for k, v in report.items() if k != 'sha256'}) + + def test_installed_contract_hashes_and_legacy_projection_are_unchanged(self): + # Frozen from the verified baseline360bb, not generated by this implementation. + hashes = {1: '81515fd903aefd3ab717a8a9b7cfb937726a312b1fa4808bec8a06ae03094125', + 2: '7b287616c67d154615980c239c418f54764f7ffd8fef2430d13d582dc8319951', + 3: 'ed0796b3c889f042966ec9308d9c5aebfc13031b668d42654de98dd892f5c96c'} + for version, sha in hashes.items(): + self.assertEqual(api.contract('graphs.preflight_external', version)['sha256'], sha) + self.assertEqual(api.discovery()['digest'], 'cb070a69ebd35b48e4f95a57ca01647c75bd7567521429e49efc3a3a1daebae8') + + def test_refusals_preserve_typed_outcome_and_only_request_provenance(self): + for code, state in (('invalid', 'invalid'), ('unsupported', 'unsupported'), + ('unavailable', 'blocked'), ('stale', 'blocked'), ('storage', 'blocked')): + value = self.refusal(code); report = value['result']['preflight_report'] + self.assertEqual(api.validate_output('graphs.preflight_external', value), value) + self.assertEqual(report['state'], state) + self.assertEqual(report['refusal'], {'code': code, 'status': 409}) + self.assertIsNone(report['material_sha256']); self.assertIsNone(report['execution_sha256']) + self.assertEqual(report['admission'], 'not_reserved') + self.assertLess(len(json.dumps(report).encode()), 8192) + self.assertLessEqual(len(report['checks']), 16) + self.assertFalse(any(row['state'] == 'checked' for row in report['checks'])) + self.assertNotIn('Private producer detail', json.dumps(report)) + self.assertTrue(any(row['code'] == 'runtime_ready_go' and row['state'] == 'deferred' + for row in report['checks'])) + + def test_domain_preserves_original_error_and_does_not_reprepare_or_publish(self): + graph, owner, prepared = self.fixture.domain_owner() + owner.db = SimpleNamespace(dataset_id='b' * 32) + for version in (None, 1, 2, 3, 4): + error = WorkbenchError('Runtime unavailable', 'unavailable', 409) + with patch.object(pipeline.recipe_contract, 'prepare', side_effect=error) as prepare: + with self.assertRaises(WorkbenchError) as caught: + pipeline.Pipelines.preflight_external(owner, graph['id'], 2, contract_version=version) + self.assertIs(caught.exception, error); self.assertEqual(prepare.call_count, 1) + self.assertEqual(getattr(error, 'preflight_report', None) is not None, version == 4) + with patch.object(pipeline.recipe_contract, 'prepare', return_value=(prepared, {})): + result = pipeline.Pipelines.preflight_external(owner, graph['id'], 2, contract_version=4) + # Use the consistent material fixture for selected schema/hash validation. + report = pipeline._external_preflight_report(self.fixture.graph, self.fixture.prepared, version=2) + value = {'status': 200, 'result': dict(self.fixture.prepared, preflight_report=report)} + api.validate_output('graphs.preflight_external', value) + self.assertEqual(result['preflight_report']['format'], 'external_preflight_report_v2') + self.assertIsNone(report['refusal']) + + def test_storage_owner_and_graph_drift_do_not_reuse_checked_material(self): + graph, owner, prepared = self.fixture.domain_owner(); owner.db = SimpleNamespace(dataset_id='b' * 32) + prepared = copy.deepcopy(prepared) + prepared['material']['resources']['profile'] = pipeline.recipe_contract.LARGE_OUTPUT_PROFILE + owner.dataset.runs = Mock() + owner.dataset.runs._check_external_storage.side_effect = WorkbenchError('Disk full', 'unavailable', 409) + with patch.object(pipeline.recipe_contract, 'prepare', return_value=(prepared, {})): + with self.assertRaises(WorkbenchError) as caught: + pipeline.Pipelines.preflight_external(owner, graph['id'], 2, contract_version=4) + report = caught.exception.preflight_report + self.assertEqual((report['checks'][0]['owner'], report['checks'][0]['code']), ('runs', 'storage_capacity')) + self.assertIsNone(report['material_sha256']) + owner.get.side_effect = [graph, dict(graph, revision=3)] + prepared['material']['resources'].pop('profile') + with patch.object(pipeline.recipe_contract, 'prepare', return_value=(prepared, {})): + with self.assertRaises(WorkbenchError) as caught: + pipeline.Pipelines.preflight_external(owner, graph['id'], 2, contract_version=4) + report = caught.exception.preflight_report + self.assertEqual(report['graph_revision'], 2) + self.assertEqual(report['refusal']['code'], 'stale') + self.assertFalse(any(row['state'] == 'checked' for row in report['checks'])) + + def test_authorization_and_early_refusals_have_no_report_or_extra_graph_read(self): + graph, owner, _ = self.fixture.domain_owner() + owner.access.require.side_effect = access.AccessError('Denied') + with self.assertRaises(access.AccessError): + pipeline.Pipelines.preflight_external(owner, graph['id'], 2, contract_version=4) + owner.get.assert_not_called() + for status in (400, 401, 403, 409, 503): + api.validate_output('graphs.preflight_external', {'status': status, + 'result': {'error': 'Bare early error', 'code': 'forbidden'}}, 4) + + def test_consumers_reject_forged_proof_status_duplicates_bounds_and_hash(self): + mutations = (lambda r: r.update(material_sha256='c' * 64), + lambda r: r.update(execution_sha256='c' * 64), + lambda r: r.update(state='prepared'), + lambda r: r['refusal'].update(status=503), + lambda r: r['checks'][0].update(state='checked'), + lambda r: r['checks'].append(copy.deepcopy(r['checks'][0])), + lambda r: r['checks'][0].update(detail='x' * 241), + lambda r: r['checks'][0].update(evidence_sha256='d' * 64), + lambda r: r['checks'][0].update(owner='runs'), + lambda r: r['checks'][0].update(phase='publication'), + lambda r: r['checks'][0].update(code='storage_capacity'), + lambda r: r['checks'][0].update(boundary='A fabricated verified runtime boundary'), + lambda r: r['checks'].pop(), + lambda r: r.update(sha256='e' * 64)) + for index, mutate in enumerate(mutations): + value = self.refusal(); report = value['result']['preflight_report']; mutate(report) + if index != len(mutations)-1: self.rehash(report) + with self.subTest(index=index), self.assertRaises(api.AgentAPIError): + api.validate_output('graphs.preflight_external', value, 4) + + def test_client_correlates_error_report_without_retry_and_wrappers_retain_it(self): + client = api.AgentClient('http://127.0.0.1:12345', 'k' * 32, 'b' * 32) + client._ready = True; client._selected = {'graphs.preflight_external': 4} + value = self.refusal(); client._request = Mock(return_value=value) + self.assertEqual(client.call('graphs.preflight_external', self.arguments), value) + for arguments in ({'graph_id': 'c' * 32, 'revision': 2}, dict(self.arguments, revision=3)): + client._request.reset_mock() + with self.assertRaises(api.AgentAPIError): client.call('graphs.preflight_external', arguments) + self.assertEqual(client._request.call_count, 1) + client.catalog_id = 'c' * 32 + with self.assertRaises(api.AgentAPIError): client.call('graphs.preflight_external', self.arguments) + client.catalog_id = 'b' * 32 + with self.assertRaises(api.AgentAPIError) as caught: + WorkflowClient(client).submission_manifest('a' * 32, 2, approval_id='d' * 32, request_key='request123') + self.assertEqual(caught.exception.preflight_report, value['result']['preflight_report']) + stderr = io.StringIO() + with patch.object(api.AgentClient, 'from_config', return_value=client), redirect_stderr(stderr): + result = project_cli.main(['--config', '/unused', 'preflight', '--graph-id', 'a' * 32, '--revision', '2']) + self.assertEqual(result, 1) + self.assertEqual(json.loads(stderr.getvalue())['preflight_report'], value['result']['preflight_report']) + + @contextmanager + def transport(self, value): + class Handler(BaseHTTPRequestHandler): + def do_POST(self): + raw = json.dumps(value['result']).encode(); self.send_response(value['status']) + self.send_header('Content-Type', 'application/json'); self.send_header('Content-Length', str(len(raw))) + self.end_headers(); self.wfile.write(raw) + def log_message(self, *args): pass + server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) + thread = threading.Thread(target=server.serve_forever); thread.start() + try: yield 'http://127.0.0.1:' + str(server.server_port) + finally: server.shutdown(); server.server_close(); thread.join() + + def test_real_http_error_normalization_preserves_only_selected_report_and_redacts(self): + value = self.refusal(); value['result'].update(unrelated='discard', error='k' * 32) + with self.transport(value) as base: + client = api.AgentClient(base, 'k' * 32, 'b' * 32) + client._ready = True + for version in (3, 4): + client._selected = {'graphs.preflight_external': version} + result = client.call('graphs.preflight_external', self.arguments)['result'] + self.assertEqual('preflight_report' in result, version == 4) + self.assertNotIn('unrelated', result); self.assertEqual(result['error'], '[redacted]') + report = value['result']['preflight_report'] + value['result']['preflight_report'] = None + with self.assertRaises(api.AgentAPIError): + client.call('graphs.preflight_external', self.arguments) + value['result']['preflight_report'] = report + value['result']['preflight_report']['checks'][0]['detail'] = 'k' * 32 + with self.assertRaises(api.AgentAPIError) as caught: + client.call('graphs.preflight_external', self.arguments) + self.assertNotIn('k' * 32, str(caught.exception)) + + +@unittest.skipUnless(os.environ.get('TULDOK_DATABASE_URL'), 'Requires disposable PostgreSQL') +class PreflightRefusalHTTPTests(unittest.TestCase): + def test_actual_owner_refusal_and_old_http_contracts_leave_execution_counts_unchanged(self): + import test_agent_project_snapshots as snapshots + case = snapshots.AgentSnapshotHTTPTests('runTest') + self.addCleanup(case.doCleanups); case.setUp() + self.schema = case.dataset.db.schema + graph = case.call('graphs.create_external', case.graph_body(), 201) + before = case.counts() + # Actual production preparation observes the fixture's unavailable runtime. + with patch('subprocess.Popen', side_effect=AssertionError('Preflight cannot execute a child')): + value = case.client.call('graphs.preflight_external', {'graph_id': graph['id'], 'revision': 1}) + self.assertGreaterEqual(value['status'], 400) + report = value['result']['preflight_report'] + self.assertIsNone(report['material_sha256']); self.assertEqual(report['admission'], 'not_reserved') + for version in (1, 2, 3): + status, old = case.http('graphs.preflight_external', {'graph_id': graph['id'], 'revision': 1}, + version=version, token=case.owner.token) + self.assertEqual(status, value['status']); self.assertEqual(old['code'], value['result']['code']) + self.assertNotIn('preflight_report', old) + status, old = case.http('graphs.preflight_external', {'graph_id': graph['id'], 'revision': 1}, + selected=False, token=case.owner.token) + self.assertEqual(status, value['status']); self.assertNotIn('preflight_report', old) + stderr = io.StringIO() + with patch.object(api.AgentClient, 'from_config', return_value=case.client), redirect_stderr(stderr): + exit_code = project_cli.main(['--config', '/unused', 'preflight', '--graph-id', graph['id'], '--revision', '1']) + self.assertEqual(exit_code, 1) + self.assertEqual(json.loads(stderr.getvalue())['preflight_report'], report) + with patch.object(pipeline.recipe_contract, 'prepare') as prepare: + status, denied = case.http('graphs.preflight_external', {'graph_id': graph['id'], 'revision': 1}, + token='x' * 32) + self.assertEqual(status, 401); self.assertNotIn('preflight_report', denied) + prepare.assert_not_called() + self.assertEqual(case.counts(), before) + + +if __name__ == '__main__': unittest.main() diff --git a/tests/test_run_preflight_report.py b/tests/test_run_preflight_report.py index 53d342a7..4adc933d 100644 --- a/tests/test_run_preflight_report.py +++ b/tests/test_run_preflight_report.py @@ -68,7 +68,7 @@ def test_new_wire_contract_requires_closed_bounded_report_and_old_overlap_surviv for version in (1, 2, 3): agent_api.validate_output('graphs.preflight_external', {'status': 200, 'result': result}, version) selected = agent_api.negotiate(agent_api.capabilities('b' * 32), 'b' * 32) - self.assertEqual(selected['graphs.preflight_external'], 3) + self.assertEqual(selected['graphs.preflight_external'], 4) for mutate in (lambda value: value.pop('preflight_report'), lambda value: value.update(valid=False), lambda value: value.update(status='blocked'), @@ -165,7 +165,7 @@ def test_graph_drift_or_preparation_failure_cannot_emit_checked_report(self): report.assert_not_called() def test_unsupported_report_versions_refuse_before_preparation(self): - for version in (True, 1.0, '3', 4): + for version in (True, 1.0, '3', 5): graph, owner, _prepared = self.domain_owner() with patch.object(pipeline.recipe_contract, 'prepare') as prepare: with self.subTest(version=version), self.assertRaises(WorkbenchError) as caught: diff --git a/tuldok_client.py b/tuldok_client.py index d5f9eaba..a2103a5b 100644 --- a/tuldok_client.py +++ b/tuldok_client.py @@ -55,7 +55,8 @@ def _invoke(client, operation, arguments): envelope = client.call(operation, arguments) if envelope['status'] >= 400: error = envelope['result'] - raise AgentAPIError(error['error'], error.get('code', 'http_error'), envelope['status']) + raise AgentAPIError(error['error'], error.get('code', 'http_error'), envelope['status'], + preflight_report=error.get('preflight_report')) return envelope['result'] From cd420c44c42ff85487965a858865170cfc0a9358 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 18:20:27 +0000 Subject: [PATCH 2/2] feat(pipeline): report retained material preflight outcomes --- agent_api.py | 62 +- app.py | 9 + docs/contracts/agent-operations.md | 8 + docs/contracts/project-cli.md | 8 + docs/contracts/run-preflight-publication.md | 59 +- .../execution-ledger.md | 34 + .../acceptance.md | 60 ++ .../all-prior-contracts.json | 776 ++++++++++++++++++ .../final-source.json | 11 + .../independent-review.json | 134 +++ .../material-followup-result.json | 14 + .../material-followup-runner.py | 40 + .../material-followup.log | 19 + .../preservation.json | 10 + .../qualification.json | 99 +++ .../regression-result.json | 20 + .../regression-runner.py | 45 + .../regression-source.json | 10 + .../regression.log | 98 +++ pipeline.py | 39 +- tests/test_material_preflight_reports.py | 252 ++++++ 21 files changed, 1799 insertions(+), 8 deletions(-) create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/acceptance.md create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/all-prior-contracts.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/final-source.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/independent-review.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-result.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-runner.py create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup.log create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/preservation.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/qualification.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-result.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-runner.py create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-source.json create mode 100644 docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression.log create mode 100644 tests/test_material_preflight_reports.py diff --git a/agent_api.py b/agent_api.py index 7e7cd9aa..0aa9a18b 100644 --- a/agent_api.py +++ b/agent_api.py @@ -338,6 +338,8 @@ def model_operation(name,method,path,properties=None,required=None,*,output=None {'model_id':ID,'revision':SAFE_POS,'reconcile_capture':{'type':'boolean'}},['model_id','revision'],scopes=('inspect','edit'),mutation=True) model_operation('models.material','POST','/api/workspace/model-material',{'ref':MODEL_REF}) model_operation('recipes.material','POST','/api/workspace/recipe-material',{'ref':MATERIAL_REF}) +model_operation('recipes.preflight_material','POST','/api/workspace/recipe-material/preflight',{'ref':MATERIAL_REF}, + description='Revalidate exact retained external Study material without execution, grant or reservation; native builtin probes are unsupported.') model_operation('studies.trial','GET','/api/workspace/trials/{trial_id}',{'trial_id':ID}) model_operation('studies.create','POST','/api/workspace/studies',{'name':text(120),'limits':BUDGET,'policy':SELECTION_POLICY}, scopes=('inspect','edit'),mutation=True) @@ -925,6 +927,21 @@ def model_operation(name,method,path,properties=None,required=None,*,output=None _preflight_refusal_contract['outputSchema']['oneOf'][1]['properties']['result']['properties']['preflight_report'] = PREFLIGHT_REPORT_V2 _preflight_refusal_contract['description'] += ' Preserve typed preparation refusals with bounded request-only reports; unverified material identities remain null.' _CONTRACTS['graphs.preflight_external'][4] = _preflight_refusal_contract +PREFLIGHT_REPORT_V3 = copy.deepcopy(PREFLIGHT_REPORT_V2) +PREFLIGHT_REPORT_V3['properties'].update( + format=const('external_preflight_report_v3'), + graph_id={'oneOf': [ID, {'type': 'null'}]}, + graph_revision={'oneOf': [integer(1), {'type': 'null'}]}, + recipe_material_ref=MATERIAL_REF, + model_ref={'oneOf': [MODEL_REF, {'type': 'null'}]}, + execution_material_sha256={'oneOf': [SHA, {'type': 'null'}]}) +PREFLIGHT_REPORT_V3['required'].extend(('recipe_material_ref','model_ref','execution_material_sha256')) +_material_preflight_contract = _CONTRACTS['recipes.preflight_material'][1] +_material_preflight_success = copy.deepcopy(_NEW_OUTPUTS['recipes.material']) +_material_preflight_success['properties']['preflight_report'] = PREFLIGHT_REPORT_V3 +_material_preflight_success['required'].append('preflight_report') +_material_preflight_contract['outputSchema']['oneOf'][0]['properties']['result'] = _material_preflight_success +_material_preflight_contract['outputSchema']['oneOf'][1]['properties']['result']['properties']['preflight_report'] = PREFLIGHT_REPORT_V3 # Managed Torch extends the recipe/runtime variants without rewriting the # independently deployed v2/v3 inspection contracts. _managed_inspection=copy.deepcopy(_CONTRACTS['recipes.inspect'][3]) @@ -1290,7 +1307,8 @@ def validate_output(name, envelope, version=None): or value['original_fault']['source_sha256'] != source['sha256']): raise AgentAPIError('Recovered publication result identity is inconsistent.', 'transport', 502) selected_version = max(_CONTRACTS[op['operation_id']]) if version is None else version - if (op['operation_id'] == 'graphs.preflight_external' and selected_version == 4 + material_preflight = op['operation_id'] == 'recipes.preflight_material' + if ((op['operation_id'] == 'graphs.preflight_external' and selected_version == 4 or material_preflight) and 'preflight_report' in envelope['result']): report = envelope['result']['preflight_report'] content = lambda value: hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':'), @@ -1327,6 +1345,11 @@ def validate_output(name, envelope, version=None): or refusal['status'] in (401, 403) or refusal['code'] != envelope['result'].get('code') or report['state'] != expected or report['material_sha256'] is not None or report['execution_sha256'] is not None + or material_preflight and (report['graph_id'] is not None or report['graph_revision'] is not None + or report['model_ref'] is not None or report['execution_material_sha256'] is not None + or report['recipe_material_ref']['catalog_id'] != report['catalog_id'] + or report['recipe_material_ref']['id'] != report['recipe_material_ref']['sha256'] + or failed != 'material_preparation') or set(layout) != {row['code'] for row in report['checks']} or any(row['state'] != (expected if row['code'] == failed else 'deferred') or (row['owner'], row['phase'], row['boundary']) != layout.get(row['code']) @@ -1336,6 +1359,37 @@ def validate_output(name, envelope, version=None): elif report['refusal'] is not None or report['state'] != 'prepared' or any( row['state'] not in ('checked', 'deferred') for row in report['checks']): raise AgentAPIError('Prepared preflight report contradicts its outcome.', 'transport', 502) + if material_preflight and envelope['status'] < 400: + value=envelope['result'];report=value['preflight_report'];material=value['material'];ref=value['ref'] + checks={row['code']:row for row in report['checks']} + layout={'source_inputs':('pipeline','preparation','checked'), + 'runtime_inventory':('pipeline','preparation','checked'), + 'host_closure':('pipeline','preparation','checked'), + 'output_contract':('pipeline','preparation','checked'), + 'capture_authority':('pipeline','preparation','checked'), + 'authority_admission':('runs','admission','deferred'), + 'resource_availability':('runs','admission','deferred'), + 'runtime_ready_go':('runs','dispatch','deferred'), + 'custody_completion':('runs','execution','deferred'), + 'output_publication':('runs','publication','deferred')} + if (ref != report['recipe_material_ref'] or ref['id'] != ref['sha256'] + or ref['sha256'] != content(material) or report['material_sha256'] != ref['sha256'] + or report['catalog_id'] != ref['catalog_id'] or material['catalog_id'] != ref['catalog_id'] + or report['model_ref'] != material['model'] or material['model']['catalog_id'] != ref['catalog_id'] + or any(material[key]['catalog_id'] != ref['catalog_id'] for key in ('trainer','evaluation_profile','runtime')) + or material['trainer']['id'] not in ('external_recipe_v1','external_model_evidence_recipe_v1') + or material['data'] != [] or material['runtime']['id'] != material['runtime']['sha256'] + or report['execution_material_sha256'] != material['executable_sha256'] + or report['execution_sha256'] is None + or report['graph_id'] != material['recipe_id'] or report['graph_revision'] != material['recipe_revision'] + or set(layout) != {row['code'] for row in report['checks']} + or any((row['owner'],row['phase'],row['state']) != layout[row['code']] for row in report['checks']) + or any(checks[code]['evidence_sha256'] != material['runtime']['sha256'] + for code in ('runtime_inventory','runtime_ready_go')) + or any(checks[code]['evidence_sha256'] != material['executable_sha256'] + for code in ('authority_admission','custody_completion')) + or checks['output_publication']['evidence_sha256'] != checks['output_contract']['evidence_sha256']): + raise AgentAPIError('Retained material preflight identity is inconsistent.', 'transport', 502) if (op['operation_id'] == 'graphs.preflight_external' and selected_version in (3, 4) and envelope['status'] < 400): result = envelope['result']; report = result['preflight_report']; material = result['material'] @@ -1582,7 +1636,7 @@ def watch_read(): if (type(code) is not str or not re.fullmatch('[a-z][a-z0-9_]{0,79}', code) or self._token in code): raise AgentAPIError('Malformed HTTP error code.', 'transport', 502) - has_report = selection == ('graphs.preflight_external', 4) and 'preflight_report' in value + has_report = selection in (('graphs.preflight_external', 4), ('recipes.preflight_material', 1)) and 'preflight_report' in value report = value['preflight_report'] if has_report else None if has_report and self._token in json.dumps(report, ensure_ascii=False): raise AgentAPIError('Preflight report contains credential material.', 'transport', 502) @@ -1796,6 +1850,10 @@ def call(self, name, arguments, *, observer=None, observation=None): if (report['graph_id'] != arguments['graph_id'] or report['graph_revision'] != arguments['revision'] or report['catalog_id'] != self.catalog_id): raise AgentAPIError('Preflight report does not match the requested graph or catalog.', 'transport', 502) + if op['operation_id'] == 'recipes.preflight_material' and 'preflight_report' in result['result']: + report=result['result']['preflight_report'] + if report['recipe_material_ref'] != arguments['ref'] or report['catalog_id'] != self.catalog_id: + raise AgentAPIError('Preflight report does not match the requested material or catalog.', 'transport', 502) return result diff --git a/app.py b/app.py index 93864efd..d4e39bfa 100644 --- a/app.py +++ b/app.py @@ -1071,6 +1071,15 @@ def source_guard(source): self._snapshot_contract_access(source['source_selector']['project_id'], 'models.publish',2,'edit') return self.reply(dataset.models.publish(parts[1],body,_source_guard=source_guard)) + if parts == ['recipe-material','preflight']: + agent_api.require_selection(self._contract_selection,'recipes.preflight_material') + projects.closed(body,('ref',)) + try: + return self.reply(dataset.pipelines.preflight_recipe_material(body['ref'],report=True)) + except workbench.WorkbenchError as error: + report=getattr(error,'preflight_report',None) + if report is None:raise + return self.reply({'error':str(error),'code':error.code,'preflight_report':report},error.status) if parts in (['model-material'],['recipe-material']): model=parts==['model-material'];name='models.material' if model else 'recipes.material' agent_api.require_selection(self._contract_selection,name);projects.closed(body,('ref',)) diff --git a/docs/contracts/agent-operations.md b/docs/contracts/agent-operations.md index fb4455e9..3447b07e 100644 --- a/docs/contracts/agent-operations.md +++ b/docs/contracts/agent-operations.md @@ -69,6 +69,14 @@ and configured catalog. Its `prepared`/`not_reserved` state leaves admission, device READY/GO, actual output validation and publication explicitly deferred. No execution authority, retry or new client dependency is introduced. +`recipes.preflight_material/1` adds an inspect-only exact retained external +Study-material read at `/api/workspace/recipe-material/preflight` with +`{"ref": MATERIAL_REF}`. Its closed success result adds the same versioned +[bounded report family](run-preflight-publication.md#exact-retained-study-material-preflight) +to the existing material projection; typed refusals carry request-only provenance. +Builtin native probes are unsupported. Existing cold material reads, Trial receipts +and all prior contract identities remain unchanged. + Selected `graphs.preflight_external/4` retains those versions and adds the [versioned refusal projection](run-preflight-publication.md#selected-v4-refusal-reports) to classified preparation errors. It preserves the original HTTP/code outcome; diff --git a/docs/contracts/project-cli.md b/docs/contracts/project-cli.md index cde0626c..d788385b 100644 --- a/docs/contracts/project-cli.md +++ b/docs/contracts/project-cli.md @@ -146,6 +146,14 @@ GPU use or remote execution. `python project_cli.py --config PRIVATE_CONFIG call --operation models.get --arguments model.json` uses the same installed registry, exact capability selection and bounded JSON argument reader. For example, `model.json` contains `{"model_id":"OPAQUE_ID"}`. +For a fresh, inert external retained-material check, write +`{"ref":RETAINED_MATERIAL_REF}` from `studies.trial.material` to a regular JSON +file and call `--operation recipes.preflight_material --arguments FILE` through +the existing `call` command. It returns the selected v3 bounded report or retains +the validated typed refusal report in stderr JSON with a nonzero exit status. +Builtin native probes are unsupported; no execution or Study reservation occurs. +See the [exact material contract](run-preflight-publication.md#exact-retained-study-material-preflight). + `recipes.material` accepts `{"ref":RETAINED_MATERIAL_REF}` and `studies.get` accepts `{"study_id":"OPAQUE_ID"}`. All print the same domain result available through selected HTTP and structured MCP. No project code executes on inspection. diff --git a/docs/contracts/run-preflight-publication.md b/docs/contracts/run-preflight-publication.md index 685f01a9..f7f7d83c 100644 --- a/docs/contracts/run-preflight-publication.md +++ b/docs/contracts/run-preflight-publication.md @@ -42,8 +42,8 @@ observation when the selected recipe uses the large-output resource profile: The fixed-array capacity checks are described in [external recipes](external-recipes.md). Headers, variable diagnostics/context, actual output sizes and science remain later checks. A preparation refusal keeps -its existing typed error and emits no successful report. Structured failure -diagnostics and complete M1 preflight/publication recovery are not yet implemented. +its existing typed error and emits no successful report. The selected refusal +and retained-material extensions below do not complete all M1 acceptance. ### Selected v4 refusal reports @@ -89,6 +89,61 @@ exit status; WorkflowClient preparation raises the same typed error carrying `preflight_report` and emits no submission manifest. Neither consumer retries. Current credential content in a received refusal report is rejected without echo. +### Exact retained Study material preflight + +`recipes.preflight_material/1` is the inspect-only POST operation at +`/api/workspace/recipe-material/preflight`, with closed input `{"ref": MATERIAL_REF}`. +Use the exact `material` reference from `studies.trial`; the operation does not +infer a Study, Trial membership, continuation state or budget from that reference. +It exposes Pipeline's existing `recipes.preflight_material` owner action without +changing `studies.prepare_trial` durable receipts or its replay behavior. + +The successful closed result extends `recipes.material`'s `{ref, material}` with +`preflight_report: external_preflight_report_v3`. Pipeline verifies retained +composition/Model/capture bytes, revalidates the stored external graph against +current source/input/runtime/host state, then rechecks current retained disclosure +before delivery. It uses the retained recipe ID/revision, even after the editable +graph advances. It neither imports authored code nor launches a child. Builtin +materials return typed `unsupported` before native preparation/probing; the +existing internal Study approval/capture path retains its original behavior. + +V3 extends the same bounded report family and preserves these distinct identities: + +| Field | Meaning | +| --- | --- | +| `recipe_material_ref`, `material_sha256` | Exact outer composition reference and canonical hash of returned `material`. | +| `model_ref` | The verified Model reference in that composition. | +| `execution_material_sha256` | Inner prepared-material digest, equal to returned `material.executable_sha256`. | +| `execution_sha256` | Graph execution digest, retaining the graph report meaning. | +| `graph_id`, `graph_revision` | Stored graph in the retained recipe snapshot. | + +The ten success checks reuse the actual inner preparation proofs. Their hashes +are not recomputed from the outer capture-blob list: that inventory has a different +representation. Runtime evidence is correlated with the returned runtime digest; +admission/custody evidence binds the inner material digest. Source, host, output +and graph-execution details remain owner-attested provenance in this bounded +projection. A client cannot independently reconstruct the graph execution digest +from `{ref, material}`. Report hashing establishes content consistency, not a +new scientific proof. No storage-capacity observation is added here; resource +availability, authority, READY/GO, custody and publication remain deferred. + +Classified refusals preserve their typed code/status and eleven-row request-only +layout. Only `recipe_material_ref` and its catalog are established: graph identity, +Model, outer/inner material and execution digests are null, including after partial +verification. No checked proof survives a refused preparation. Malformed/foreign +references, authentication/disclosure failures, missing records and unclassified +errors remain bare typed errors. No extra investigation decorates a refusal. + +AgentClient selects this additive contract, validates its closed bounds and hash, +correlates the exact requested Ref/catalog and all locally available composition +links, and rejects malformed/null reports or current credential contents in +refusal reports. +The generic CLI `call` retains validated refusal reports in stderr JSON and exits +nonzero. Neither transport retries. Existing `recipes.material` remains a cold +inspection operation; source drift can block fresh preflight while retained +inspection and old Trial receipt replay still succeed. No admission grant, +reservation, process, publication or scientific acceptance follows from this read. + The success-only/early pending statements above describe the retained v3 slice. Publication/indexed-diagnostic implementation below has its own qualification; this extension changes only graph preparation reporting. diff --git a/docs/plans/lab-workflow-reliability/execution-ledger.md b/docs/plans/lab-workflow-reliability/execution-ledger.md index 5a1db61b..44a83aaa 100644 --- a/docs/plans/lab-workflow-reliability/execution-ledger.md +++ b/docs/plans/lab-workflow-reliability/execution-ledger.md @@ -1,5 +1,39 @@ # Lab workflow reliability execution ledger +## 2026-10-20 — Local retained Study material preflight reports + +The next authorized C1 slice exposes the already named Pipeline +`recipes.preflight_material` action as an additive inspect-only public operation. +The dependent local branch `feature/material-preflight-reports-20261020` starts +at graph checkpoint `1442f1849c5894500d69823e60bfcd3d644dc683`; that branch, +all earlier evidence and unrelated README/local-agent work remain preserved. +This is the concrete retained-material gap in C1's existing acceptance contract, +not a new scientific criterion or a replacement material/schema authority. + +External material preflight reuses current retained-byte verification and live +preparation, with a final current disclosure check. The bounded v3 report binds +outer composition, Model and separately named inner material/graph execution +digests. Refusals reuse the existing typed request-only report with unknown graph, +Model and material/execution proofs. Builtin native probing is unsupported before +preparation in this read path. Internal Study approval/capture, cold inspection +and exact Trial receipts/replay retain their existing behavior. + +The direct write set is `pipeline.py`, `agent_api.py`, `app.py`, one focused test +module, three canonical contract documents and this plan's acceptance evidence. +Existing Coding Standards implementation/verification/contract guidance applies. +All 127 preceding contract hashes and the full legacy projection remain exact. +The source-frozen regression suite passed 75 tests with zero skips, failures or +errors. A separate final focused run covers the strengthened isolated adversarial +fixtures, live CLI refusal, headerless refusal and six-table inertness check; +its exact source/result and independent disposition are retained in the +[scoped acceptance report](reports/material-preflight-reports-20261020/acceptance.md). + +This slice does not close all C1/T1/M1 or add owned scientific acceptance. Native +builtin read preflight remains explicitly unsupported, and dynamic admission, +storage/device availability, custody and publication remain deferred. PUB-1 and +SCI-1 still require the actual privately owned failure/scientific inputs. No +publication, merge, branch deletion or authentication change is selected. + ## 2026-10-20 — Local graph preflight refusal reporting The user selected continuation of this canonical plan's C1/T1 structured-refusal diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/acceptance.md b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/acceptance.md new file mode 100644 index 00000000..18625fb3 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/acceptance.md @@ -0,0 +1,60 @@ +# Retained external material preflight: scoped local acceptance + +This implements the concrete C1 retained Study-material reporting gap through +`recipes.preflight_material/1`. The dependent local branch is +`feature/material-preflight-reports-20261020`, based on preserved graph checkpoint +`1442f1849c5894500d69823e60bfcd3d644dc683`. The containing local commit is the +source checkpoint; [final-source.json](final-source.json) identifies its exact +production/test bytes without a circular commit reference. + +The new inspect-only operation reuses Pipeline preparation, material/Model +verification and current transitive disclosure. The existing bounded report +family distinguishes the outer composition, inner executable material and graph +execution digests. Classified refusals retain code/status, bounded request-only +provenance and null unknown identities. Native builtin probing is unsupported +before preparation. Cold material inspection and Trial receipt/replay behavior +remain unchanged; no persistence migration or execution policy is added. + +| Verification | Actual result | +| --- | --- | +| Frozen regression subject | 75 tests, 246.263s, zero failures/errors/skips. | +| Final strengthened material tests | 9 tests, 20.883s, zero failures/errors/skips. | +| Compatibility | All 127 previous selected contract hashes and legacy discovery projection exact; only `recipes.preflight_material/1` added. | +| Preservation | All 97 prior local heads exact and all 111 prior worktrees present; graph checkpoint unchanged. | +| Disposable cleanup | Eight exact schemas removed after the regression, two after the final material run. | + +The two runs have separate source manifests. Production files are identical +between them; the follow-up only isolates adversarial fixtures and strengthens +live CLI, headerless refusal and six-table inertness coverage. The final test +bytes were not the earlier 75-case subject. Full raw logs, result receipts, +runner scripts, manifests, compatibility and preservation records accompany +[qualification.json](qualification.json). + +The actual retained external Study/HTTP cases cover stored revision after a new +draft, real source drift, cold reads, Trial replay, capture tampering and bad-token +refusal. The final CLI refusal uses that live server and actual AgentClient; +only `from_config` supplies the existing test client. The separate controlled +HTTP case exercises error normalization/redaction and CLI propagation. Late +transitive disclosure loss is an injected `AccessError` over real retained +material, not a claimed live permission-revocation experiment. Six domain table +counts and Study accounting remain unchanged. Native no-process refusal is a +mock-backed boundary test, not hardware/runtime qualification. + +Independent review required foreign-reference/runtime evidence correlations, +accurate owner-attested graph provenance and precise consumer wording. Those +findings are resolved, with final disposition in +[independent-review.json](independent-review.json). Early harness errors and the +fixture-isolation correction remain explicitly recorded; none is relabeled as +passing. Existing historical failure records remain untouched. + +This accepts only the external retained-material report slice. Full C1/T1/M1 +remains open, including builtin read coverage and remaining broader acceptance. +The graph execution/source/host/output provenance is owner-attested in this +bounded outer projection; clients cannot reconstruct the inner graph from it. +Admission, storage/device availability, custody, publication and scientific +acceptance remain deferred. PUB-1 still needs the actual private failed Run and +custody/output inputs; SCI-1 still needs an actual compatible scientific pair, +owned criterion and environment/procedure. No science or hardware pass is claimed. + +No publication, merge, branch deletion, credential change or expired CLI-auth +retry was performed. Unrelated README/local-agent branches and worktrees remain. diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/all-prior-contracts.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/all-prior-contracts.json new file mode 100644 index 00000000..75ab59ee --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/all-prior-contracts.json @@ -0,0 +1,776 @@ +{ + "baseline": "1442f1849c5894500d69823e60bfcd3d644dc683", + "prior_contract_count": 127, + "all_prior_hashes_exact": true, + "legacy_projection_exact": true, + "only_added_contracts": [ + { + "operation": "recipes.preflight_material", + "version": 1 + } + ], + "contracts": [ + { + "operation": "context.get", + "version": 1, + "baseline_sha256": "bdf3f54c5c17064dc493bda792e64f94bdb2adaa011e3f20dfeb0af4b9a09e91", + "current_sha256": "bdf3f54c5c17064dc493bda792e64f94bdb2adaa011e3f20dfeb0af4b9a09e91" + }, + { + "operation": "identity.get", + "version": 1, + "baseline_sha256": "b93197acd73d5622f325874bf80d66e34d57afc9f196690a352cc94c8a2a4561", + "current_sha256": "b93197acd73d5622f325874bf80d66e34d57afc9f196690a352cc94c8a2a4561" + }, + { + "operation": "projects.list", + "version": 1, + "baseline_sha256": "4cdd739a17078b96d32329556dfc6c4e7f91a92a1e6918513394f69596caca14", + "current_sha256": "4cdd739a17078b96d32329556dfc6c4e7f91a92a1e6918513394f69596caca14" + }, + { + "operation": "recipes.list", + "version": 1, + "baseline_sha256": "0b4322ebcaf0bf80f407cb847614abd56a5a4550ffa2b8b9e6c1bf49263fc560", + "current_sha256": "0b4322ebcaf0bf80f407cb847614abd56a5a4550ffa2b8b9e6c1bf49263fc560" + }, + { + "operation": "releases.list", + "version": 1, + "baseline_sha256": "b1b3aa41f82dc5a1dd13c5f1ac4961675dee34c886bca94f84f0fbcf042d6bf4", + "current_sha256": "b1b3aa41f82dc5a1dd13c5f1ac4961675dee34c886bca94f84f0fbcf042d6bf4" + }, + { + "operation": "releases.inputs", + "version": 1, + "baseline_sha256": "112404e67d68a2e383cfeb4391c119435e06bd9f1295e3c903123300869fdc24", + "current_sha256": "112404e67d68a2e383cfeb4391c119435e06bd9f1295e3c903123300869fdc24" + }, + { + "operation": "graphs.list", + "version": 1, + "baseline_sha256": "28ce7f42861e76af870b4a1e1914435c9d3fd5121901b9a7fd5e8d9572bf1669", + "current_sha256": "28ce7f42861e76af870b4a1e1914435c9d3fd5121901b9a7fd5e8d9572bf1669" + }, + { + "operation": "runs.list", + "version": 1, + "baseline_sha256": "5130a95df5068e81b566f08203c134599d9b9a8a08dd58bfbcd508a6aab44d3c", + "current_sha256": "5130a95df5068e81b566f08203c134599d9b9a8a08dd58bfbcd508a6aab44d3c" + }, + { + "operation": "run_inputs.list", + "version": 1, + "baseline_sha256": "65cb223921e6bfb78b0d9dbfd42db126dc5cf1b981386b033cd70620b1254250", + "current_sha256": "65cb223921e6bfb78b0d9dbfd42db126dc5cf1b981386b033cd70620b1254250" + }, + { + "operation": "projects.get", + "version": 1, + "baseline_sha256": "2bcf2160fe3424a8cb59199c1e1d47166cedd86dd819ae4db0dbec0f6230c7e5", + "current_sha256": "2bcf2160fe3424a8cb59199c1e1d47166cedd86dd819ae4db0dbec0f6230c7e5" + }, + { + "operation": "projects.open", + "version": 1, + "baseline_sha256": "1b48cb0696b08ee91c71e221bf6669b6871feb3febfd3ed831b3e8dca78dbf6d", + "current_sha256": "1b48cb0696b08ee91c71e221bf6669b6871feb3febfd3ed831b3e8dca78dbf6d" + }, + { + "operation": "projects.evidence", + "version": 1, + "baseline_sha256": "5e6f711de712b10bec6c04f66f91aca7e54fbf518eeb7fcab6d35e9db6c1455d", + "current_sha256": "5e6f711de712b10bec6c04f66f91aca7e54fbf518eeb7fcab6d35e9db6c1455d" + }, + { + "operation": "recipes.inspect", + "version": 1, + "baseline_sha256": "096de78ee573233201b14b6766e6267f6d2dff53904acc533f4a003cb2b973f8", + "current_sha256": "096de78ee573233201b14b6766e6267f6d2dff53904acc533f4a003cb2b973f8" + }, + { + "operation": "recipes.inspect", + "version": 2, + "baseline_sha256": "be0cfb596b3ae741e2903be57413684bac28e9fa3c59987086fde49e514284ea", + "current_sha256": "be0cfb596b3ae741e2903be57413684bac28e9fa3c59987086fde49e514284ea" + }, + { + "operation": "recipes.inspect", + "version": 3, + "baseline_sha256": "5b5c6a4eefe84df8da4c31d779a468c7bdbacf4296c294076010fbbaed167458", + "current_sha256": "5b5c6a4eefe84df8da4c31d779a468c7bdbacf4296c294076010fbbaed167458" + }, + { + "operation": "recipes.inspect", + "version": 4, + "baseline_sha256": "0080ae0e39bf758b9c96c3208ef1614a9335f257731cfde935a23d8b486dedb1", + "current_sha256": "0080ae0e39bf758b9c96c3208ef1614a9335f257731cfde935a23d8b486dedb1" + }, + { + "operation": "data.query", + "version": 1, + "baseline_sha256": "72737d63bfc545991afc93e4f050577bc49ab7b332029fd7e292b6555fdc6baf", + "current_sha256": "72737d63bfc545991afc93e4f050577bc49ab7b332029fd7e292b6555fdc6baf" + }, + { + "operation": "data.get", + "version": 1, + "baseline_sha256": "12486e16d1eb306d4fb005d934373a93a816174a66cb998209e3c059c49db8a6", + "current_sha256": "12486e16d1eb306d4fb005d934373a93a816174a66cb998209e3c059c49db8a6" + }, + { + "operation": "data.history", + "version": 1, + "baseline_sha256": "e6962d14b1f126eabfaa8f04d8fe2e2ddb7f1d008696bcf0029dfcc940dd4cd3", + "current_sha256": "e6962d14b1f126eabfaa8f04d8fe2e2ddb7f1d008696bcf0029dfcc940dd4cd3" + }, + { + "operation": "data.audio", + "version": 1, + "baseline_sha256": "b7ec1ef8130def12d8c1b5a23a481194ec8a0a127e1b7f2b77a3a0417ee35eb0", + "current_sha256": "b7ec1ef8130def12d8c1b5a23a481194ec8a0a127e1b7f2b77a3a0417ee35eb0" + }, + { + "operation": "data.mesh", + "version": 1, + "baseline_sha256": "f0cc78c30525eec3a08e443dd85fc7ddef5f19faa162cee3ce49d7fcbcea6b71", + "current_sha256": "f0cc78c30525eec3a08e443dd85fc7ddef5f19faa162cee3ce49d7fcbcea6b71" + }, + { + "operation": "data.pointcloud", + "version": 1, + "baseline_sha256": "cd54eeec5d3329a9873cb043303df88f48b97f4be27c8726eef4a9d6e70eadbd", + "current_sha256": "cd54eeec5d3329a9873cb043303df88f48b97f4be27c8726eef4a9d6e70eadbd" + }, + { + "operation": "data.responses", + "version": 1, + "baseline_sha256": "4a24120dfe01bd0e482786668ebee85c862978ea2e423b614c2c6e47d8d8e69c", + "current_sha256": "4a24120dfe01bd0e482786668ebee85c862978ea2e423b614c2c6e47d8d8e69c" + }, + { + "operation": "data.preferences", + "version": 1, + "baseline_sha256": "b64ead9e81f480eccb30341d895ca80d10493d17839111ca69dd571f7ce7545c", + "current_sha256": "b64ead9e81f480eccb30341d895ca80d10493d17839111ca69dd571f7ce7545c" + }, + { + "operation": "data.volume", + "version": 1, + "baseline_sha256": "4548adfac53ced635916174e167f75dc47587a59e0d6dc23cffa55ee27906b00", + "current_sha256": "4548adfac53ced635916174e167f75dc47587a59e0d6dc23cffa55ee27906b00" + }, + { + "operation": "data.scientific", + "version": 1, + "baseline_sha256": "2a2b5cf9d6a787014f9725ff0dcee8ee2594511f4b29926409066c57c69555c2", + "current_sha256": "2a2b5cf9d6a787014f9725ff0dcee8ee2594511f4b29926409066c57c69555c2" + }, + { + "operation": "data.import_image", + "version": 1, + "baseline_sha256": "661f5a74b60bc5af77f7e9de4f3e3e9470cfce264e46f371b60b073ed4d0a232", + "current_sha256": "661f5a74b60bc5af77f7e9de4f3e3e9470cfce264e46f371b60b073ed4d0a232" + }, + { + "operation": "data.import_text", + "version": 1, + "baseline_sha256": "f21a28172a64520e4fe4a21a8f638059b0daa384055dd0b532bc5fa7ba1c6af0", + "current_sha256": "f21a28172a64520e4fe4a21a8f638059b0daa384055dd0b532bc5fa7ba1c6af0" + }, + { + "operation": "data.import_audio", + "version": 1, + "baseline_sha256": "e3acba91ee3f8e08a30673e96516981c93c4a1dec22ee0739f3bee1200563512", + "current_sha256": "e3acba91ee3f8e08a30673e96516981c93c4a1dec22ee0739f3bee1200563512" + }, + { + "operation": "data.import_mesh", + "version": 1, + "baseline_sha256": "026b371bb199677de989fe9aad10736e9f774896eebde5da72dbe6592a31a548", + "current_sha256": "026b371bb199677de989fe9aad10736e9f774896eebde5da72dbe6592a31a548" + }, + { + "operation": "data.import_pointcloud", + "version": 1, + "baseline_sha256": "6ac607e1a923468cdeb2de4ac564f7c606862674d17cc4753bcd7776361c94fd", + "current_sha256": "6ac607e1a923468cdeb2de4ac564f7c606862674d17cc4753bcd7776361c94fd" + }, + { + "operation": "data.import_volume", + "version": 1, + "baseline_sha256": "f0c041109ab841bac24b96da14785d548fdbe83a1c29c2f9c6838915e81a8f02", + "current_sha256": "f0c041109ab841bac24b96da14785d548fdbe83a1c29c2f9c6838915e81a8f02" + }, + { + "operation": "data.import_sequence", + "version": 1, + "baseline_sha256": "2796d4ae42d9af2043578aa8cd42fc355a88f081dbeb1fd1ae2986a8feb47451", + "current_sha256": "2796d4ae42d9af2043578aa8cd42fc355a88f081dbeb1fd1ae2986a8feb47451" + }, + { + "operation": "data.import_project_scientific", + "version": 1, + "baseline_sha256": "178f1a7a6cfa10f6eb88269b27e286c6eb357c8820359c1ca7b51e85f8056d6a", + "current_sha256": "178f1a7a6cfa10f6eb88269b27e286c6eb357c8820359c1ca7b51e85f8056d6a" + }, + { + "operation": "data.import_run_scientific", + "version": 1, + "baseline_sha256": "716eec54d547d02116e72cff5d770cb9474839cad3d11b3a30c491eedab5f921", + "current_sha256": "716eec54d547d02116e72cff5d770cb9474839cad3d11b3a30c491eedab5f921" + }, + { + "operation": "data.save_draft", + "version": 1, + "baseline_sha256": "9eb258c9c66f332eb3f44b705fc3b2c8013f09f7e2d4ec619c1a26a33c4bb45b", + "current_sha256": "9eb258c9c66f332eb3f44b705fc3b2c8013f09f7e2d4ec619c1a26a33c4bb45b" + }, + { + "operation": "responses.save_draft", + "version": 1, + "baseline_sha256": "58a1159cd4d12b7266318602762d07ac43720ab212fc091122400bd665c2d68b", + "current_sha256": "58a1159cd4d12b7266318602762d07ac43720ab212fc091122400bd665c2d68b" + }, + { + "operation": "proposals.caption_list", + "version": 1, + "baseline_sha256": "c79b714a33ee72362bf32ad38e02a450efe08886e17a12c0ea98c482a08e47c3", + "current_sha256": "c79b714a33ee72362bf32ad38e02a450efe08886e17a12c0ea98c482a08e47c3" + }, + { + "operation": "proposals.caption_get", + "version": 1, + "baseline_sha256": "4a8568ee62c5d3d78bb103ed1ccb03e2607e313a8c21f0b26cabfcbe193f2e40", + "current_sha256": "4a8568ee62c5d3d78bb103ed1ccb03e2607e313a8c21f0b26cabfcbe193f2e40" + }, + { + "operation": "proposals.caption_decide", + "version": 1, + "baseline_sha256": "43a88c36d98c7ee850147dbeb0f7c8049ede5b0f91bb3bf45a3a07afb468fe04", + "current_sha256": "43a88c36d98c7ee850147dbeb0f7c8049ede5b0f91bb3bf45a3a07afb468fe04" + }, + { + "operation": "proposals.classification_list", + "version": 1, + "baseline_sha256": "b2a90f8ca2514d54e2a7dee096e5fc98365906879599453313301f52a8227831", + "current_sha256": "b2a90f8ca2514d54e2a7dee096e5fc98365906879599453313301f52a8227831" + }, + { + "operation": "proposals.classification_get", + "version": 1, + "baseline_sha256": "c0e716a3329a3eedddb92abc23cc542e29defc3b5190c0dd9853908267d8fc86", + "current_sha256": "c0e716a3329a3eedddb92abc23cc542e29defc3b5190c0dd9853908267d8fc86" + }, + { + "operation": "proposals.classification_decide", + "version": 1, + "baseline_sha256": "856d53554c99b4dade1928ca98a3af729f95582cd6c450b2b04a607fa92945a6", + "current_sha256": "856d53554c99b4dade1928ca98a3af729f95582cd6c450b2b04a607fa92945a6" + }, + { + "operation": "releases.preview", + "version": 1, + "baseline_sha256": "855120f88e996d5fd165d15038c9a2900e33b77d26750ed15412a9fa35eaf5ad", + "current_sha256": "855120f88e996d5fd165d15038c9a2900e33b77d26750ed15412a9fa35eaf5ad" + }, + { + "operation": "releases.freeze", + "version": 1, + "baseline_sha256": "46c47631fe9e68bc8ffb5317194a78c78f346a791632699d14f727b1fbbb29d6", + "current_sha256": "46c47631fe9e68bc8ffb5317194a78c78f346a791632699d14f727b1fbbb29d6" + }, + { + "operation": "graphs.get", + "version": 1, + "baseline_sha256": "3bdb79835c5df6b1c5a893165a46e656201bd50e9dcb7123f1044534e5b4be7e", + "current_sha256": "3bdb79835c5df6b1c5a893165a46e656201bd50e9dcb7123f1044534e5b4be7e" + }, + { + "operation": "graphs.create", + "version": 1, + "baseline_sha256": "fba1add7dca037da902d2890f41fd682b021c381f9be92c12ff749adb669604f", + "current_sha256": "fba1add7dca037da902d2890f41fd682b021c381f9be92c12ff749adb669604f" + }, + { + "operation": "graphs.create", + "version": 2, + "baseline_sha256": "a4c0ec40212ce85ad67c5f279fa90fe4564048d4ab41b6769670cbcf21bbb615", + "current_sha256": "a4c0ec40212ce85ad67c5f279fa90fe4564048d4ab41b6769670cbcf21bbb615" + }, + { + "operation": "graphs.create", + "version": 3, + "baseline_sha256": "4badcdff944e7f37824f2424573c620a45a5f63f227f92fc9268cd3c3b5ad944", + "current_sha256": "4badcdff944e7f37824f2424573c620a45a5f63f227f92fc9268cd3c3b5ad944" + }, + { + "operation": "graphs.create_external", + "version": 1, + "baseline_sha256": "219f3c28bcb73e6d5445bdbd9b97e563bde3bd90487796b4680f36e02d338ffa", + "current_sha256": "219f3c28bcb73e6d5445bdbd9b97e563bde3bd90487796b4680f36e02d338ffa" + }, + { + "operation": "graphs.create_external", + "version": 2, + "baseline_sha256": "c21453911c25a1751c0e2532135341f59f48ed63be4e1ff1e40690ee4e471792", + "current_sha256": "c21453911c25a1751c0e2532135341f59f48ed63be4e1ff1e40690ee4e471792" + }, + { + "operation": "graphs.save", + "version": 1, + "baseline_sha256": "2791331c9628e6159f210b4ef88924dd1c44c8de8bea2d6f4aa830ddc12e81af", + "current_sha256": "2791331c9628e6159f210b4ef88924dd1c44c8de8bea2d6f4aa830ddc12e81af" + }, + { + "operation": "graphs.save", + "version": 2, + "baseline_sha256": "da60657d6c3298db8b9bbdfc6d30577125c8a6310573cee5c2feefa3a67e3564", + "current_sha256": "da60657d6c3298db8b9bbdfc6d30577125c8a6310573cee5c2feefa3a67e3564" + }, + { + "operation": "graphs.save", + "version": 3, + "baseline_sha256": "4aea6bd80d0aaa8e93eb81e2bd8a7d00a36e61317abaeeb47175a9bce38a4a24", + "current_sha256": "4aea6bd80d0aaa8e93eb81e2bd8a7d00a36e61317abaeeb47175a9bce38a4a24" + }, + { + "operation": "graphs.clone", + "version": 1, + "baseline_sha256": "caff868389b4bb3d7e2ccc3716e855f1efea18cb2ebdf602a30dfb83caac4b79", + "current_sha256": "caff868389b4bb3d7e2ccc3716e855f1efea18cb2ebdf602a30dfb83caac4b79" + }, + { + "operation": "graphs.validate", + "version": 1, + "baseline_sha256": "78a8dae94d36bc98dd874eba89af381f3a34aff06d3f0892cee4beddd7f9452f", + "current_sha256": "78a8dae94d36bc98dd874eba89af381f3a34aff06d3f0892cee4beddd7f9452f" + }, + { + "operation": "graphs.preflight_external", + "version": 1, + "baseline_sha256": "81515fd903aefd3ab717a8a9b7cfb937726a312b1fa4808bec8a06ae03094125", + "current_sha256": "81515fd903aefd3ab717a8a9b7cfb937726a312b1fa4808bec8a06ae03094125" + }, + { + "operation": "graphs.preflight_external", + "version": 2, + "baseline_sha256": "7b287616c67d154615980c239c418f54764f7ffd8fef2430d13d582dc8319951", + "current_sha256": "7b287616c67d154615980c239c418f54764f7ffd8fef2430d13d582dc8319951" + }, + { + "operation": "graphs.preflight_external", + "version": 3, + "baseline_sha256": "ed0796b3c889f042966ec9308d9c5aebfc13031b668d42654de98dd892f5c96c", + "current_sha256": "ed0796b3c889f042966ec9308d9c5aebfc13031b668d42654de98dd892f5c96c" + }, + { + "operation": "graphs.preflight_external", + "version": 4, + "baseline_sha256": "827215f788281d33b5a7edc6bf7fbf4fd985c034f454e076763e60c4972af518", + "current_sha256": "827215f788281d33b5a7edc6bf7fbf4fd985c034f454e076763e60c4972af518" + }, + { + "operation": "runs.get", + "version": 1, + "baseline_sha256": "74677042379321827f3ce590bdff656bf7504099d34d01bc98ef21baf35a0ee8", + "current_sha256": "74677042379321827f3ce590bdff656bf7504099d34d01bc98ef21baf35a0ee8" + }, + { + "operation": "runs.progress", + "version": 1, + "baseline_sha256": "3a647993b2eeda126be8b5dbbdab885dcf72612816565c56d696597154bd655a", + "current_sha256": "3a647993b2eeda126be8b5dbbdab885dcf72612816565c56d696597154bd655a" + }, + { + "operation": "runs.predictions", + "version": 1, + "baseline_sha256": "66d80ec732561adc33ce91270e28005cf33f988844dc9a7915e3e36212b5dc71", + "current_sha256": "66d80ec732561adc33ce91270e28005cf33f988844dc9a7915e3e36212b5dc71" + }, + { + "operation": "runs.submit_external", + "version": 1, + "baseline_sha256": "c3688c9c64cc8115ecd0ad9d0e13a9661aff1a7b8ff2c1acea98a82fc60afd4e", + "current_sha256": "c3688c9c64cc8115ecd0ad9d0e13a9661aff1a7b8ff2c1acea98a82fc60afd4e" + }, + { + "operation": "runs.cancel", + "version": 1, + "baseline_sha256": "6136113e34115dfef7c1de06f7ca0ee38397cdee5d54e00ce5bd186721c987ef", + "current_sha256": "6136113e34115dfef7c1de06f7ca0ee38397cdee5d54e00ce5bd186721c987ef" + }, + { + "operation": "runs.expose_test", + "version": 1, + "baseline_sha256": "08d36f1708a4299b4fb38589ab688ec648fc7326fd574870b8f1866dbe6b7338", + "current_sha256": "08d36f1708a4299b4fb38589ab688ec648fc7326fd574870b8f1866dbe6b7338" + }, + { + "operation": "runs.artifact_chunk", + "version": 1, + "baseline_sha256": "6dc1d29d698949b9a6439c65e501f9e30d0395c534aedc933a00efed8a5d3dbd", + "current_sha256": "6dc1d29d698949b9a6439c65e501f9e30d0395c534aedc933a00efed8a5d3dbd" + }, + { + "operation": "models.list", + "version": 1, + "baseline_sha256": "7a58a4f7e429c06f0c0488b8ca8a07b2901a864c7c24e1187bbd679ad0ac089a", + "current_sha256": "7a58a4f7e429c06f0c0488b8ca8a07b2901a864c7c24e1187bbd679ad0ac089a" + }, + { + "operation": "models.get", + "version": 1, + "baseline_sha256": "79b0ac2317f62ea368e8540f63c093eea2d99dd7833109037e2e3e8fa577962a", + "current_sha256": "79b0ac2317f62ea368e8540f63c093eea2d99dd7833109037e2e3e8fa577962a" + }, + { + "operation": "studies.list", + "version": 1, + "baseline_sha256": "77c21d405c0e556341ea4212fbab927a6298be114c202a43e1cdcc409a32c48a", + "current_sha256": "77c21d405c0e556341ea4212fbab927a6298be114c202a43e1cdcc409a32c48a" + }, + { + "operation": "studies.get", + "version": 1, + "baseline_sha256": "12080dea124eb9e763ec495e070211b60a219c47cab9488218383a7d3919533d", + "current_sha256": "12080dea124eb9e763ec495e070211b60a219c47cab9488218383a7d3919533d" + }, + { + "operation": "models.create", + "version": 1, + "baseline_sha256": "693d1514f44e92bb6c6635427db822acfc079a6e438d5d721630087157d1b8d5", + "current_sha256": "693d1514f44e92bb6c6635427db822acfc079a6e438d5d721630087157d1b8d5" + }, + { + "operation": "models.save", + "version": 1, + "baseline_sha256": "093806e289da9fe7df57b021cd6eb4efb2e574a687bd7337656ddd6276367c25", + "current_sha256": "093806e289da9fe7df57b021cd6eb4efb2e574a687bd7337656ddd6276367c25" + }, + { + "operation": "models.publish", + "version": 1, + "baseline_sha256": "bb84c1fd72b3617135a8246353db797bba270430121fbae6f9d46cdbfdd26235", + "current_sha256": "bb84c1fd72b3617135a8246353db797bba270430121fbae6f9d46cdbfdd26235" + }, + { + "operation": "models.publish", + "version": 2, + "baseline_sha256": "f45adea548494626fd966d29242dab71a0656d5ee7fac563f661b1e80d80aa9f", + "current_sha256": "f45adea548494626fd966d29242dab71a0656d5ee7fac563f661b1e80d80aa9f" + }, + { + "operation": "models.material", + "version": 1, + "baseline_sha256": "ab0986561fc5a738b41131d7128e65e9032588f615ccf61fd65722e80f767202", + "current_sha256": "ab0986561fc5a738b41131d7128e65e9032588f615ccf61fd65722e80f767202" + }, + { + "operation": "recipes.material", + "version": 1, + "baseline_sha256": "d3ded92c05a1e3d7d88392355e4c61b480b63e55cf763774e2823462774c2870", + "current_sha256": "d3ded92c05a1e3d7d88392355e4c61b480b63e55cf763774e2823462774c2870" + }, + { + "operation": "studies.trial", + "version": 1, + "baseline_sha256": "e53b6231eb96f09569e44e9210acf7b11c42a3fca14f35b161d58a2b36303e93", + "current_sha256": "e53b6231eb96f09569e44e9210acf7b11c42a3fca14f35b161d58a2b36303e93" + }, + { + "operation": "studies.create", + "version": 1, + "baseline_sha256": "8c4b35bbd1c2b7c6825787b40e40b2b3d22f58118ccaf1ef5f0d9bc12edb38f1", + "current_sha256": "8c4b35bbd1c2b7c6825787b40e40b2b3d22f58118ccaf1ef5f0d9bc12edb38f1" + }, + { + "operation": "studies.save", + "version": 1, + "baseline_sha256": "c5a0eee9a12d91f88b3e7dda4ee3c013c263f20897006ca50aed5f703d6c3d74", + "current_sha256": "c5a0eee9a12d91f88b3e7dda4ee3c013c263f20897006ca50aed5f703d6c3d74" + }, + { + "operation": "studies.prepare_trial", + "version": 1, + "baseline_sha256": "516f89e4a03cafee9d50ac3910d1906fb34d51d3b21e8271a4be45075ea3778a", + "current_sha256": "516f89e4a03cafee9d50ac3910d1906fb34d51d3b21e8271a4be45075ea3778a" + }, + { + "operation": "studies.prepare_trial", + "version": 2, + "baseline_sha256": "6700a5b4ce949a204073d5ac1fc9015daea16c3420ebbdd360b7baadb7440093", + "current_sha256": "6700a5b4ce949a204073d5ac1fc9015daea16c3420ebbdd360b7baadb7440093" + }, + { + "operation": "studies.continue", + "version": 1, + "baseline_sha256": "44237f86e49135203613b40a7bf6fe0960581ed7c3f128150b9576a07b491008", + "current_sha256": "44237f86e49135203613b40a7bf6fe0960581ed7c3f128150b9576a07b491008" + }, + { + "operation": "studies.approve", + "version": 1, + "baseline_sha256": "5ed2d9db58231f14c164a76f1c64157260902d2cd86f5e86433f29fc00f4f408", + "current_sha256": "5ed2d9db58231f14c164a76f1c64157260902d2cd86f5e86433f29fc00f4f408" + }, + { + "operation": "studies.admit", + "version": 1, + "baseline_sha256": "9c80abdfc50d56de88ada0141ba6c3b503cc07a395b0ceb930fa29169bd182ed", + "current_sha256": "9c80abdfc50d56de88ada0141ba6c3b503cc07a395b0ceb930fa29169bd182ed" + }, + { + "operation": "studies.stop", + "version": 1, + "baseline_sha256": "1e6d127c4d74a38a36208d513590ebf3de16a286d4316ffd766b60f60c99de9d", + "current_sha256": "1e6d127c4d74a38a36208d513590ebf3de16a286d4316ffd766b60f60c99de9d" + }, + { + "operation": "studies.reconcile", + "version": 1, + "baseline_sha256": "a209efd79a85bdb9ff5f42c469413cff9ebcc56c55ab16aa76227427670565af", + "current_sha256": "a209efd79a85bdb9ff5f42c469413cff9ebcc56c55ab16aa76227427670565af" + }, + { + "operation": "recipes.list_drafts", + "version": 1, + "baseline_sha256": "98dc2795d2081b822134bbc2c1055673850273ee20e36bfba910f255b6290e59", + "current_sha256": "98dc2795d2081b822134bbc2c1055673850273ee20e36bfba910f255b6290e59" + }, + { + "operation": "recipes.get", + "version": 1, + "baseline_sha256": "cfa012d17ba485b87a5c624d8f7d6a2e98eff95aab590d093f27aab82b1f9413", + "current_sha256": "cfa012d17ba485b87a5c624d8f7d6a2e98eff95aab590d093f27aab82b1f9413" + }, + { + "operation": "recipes.create", + "version": 1, + "baseline_sha256": "02f50bb3a87200e6651bd22db17d7801b9bf76b93ad75dcfbdaf77393350bf20", + "current_sha256": "02f50bb3a87200e6651bd22db17d7801b9bf76b93ad75dcfbdaf77393350bf20" + }, + { + "operation": "recipes.save", + "version": 1, + "baseline_sha256": "8c8016cc75974a1fc77df4c59c0afd6c3151a9377d83937c7a52d6a58ec11dcd", + "current_sha256": "8c8016cc75974a1fc77df4c59c0afd6c3151a9377d83937c7a52d6a58ec11dcd" + }, + { + "operation": "studies.select_candidate", + "version": 1, + "baseline_sha256": "940d670ae6e4630572d1c5cdeda1cc658a7b73587bedd277af137b77dac3e582", + "current_sha256": "940d670ae6e4630572d1c5cdeda1cc658a7b73587bedd277af137b77dac3e582" + }, + { + "operation": "studies.candidate", + "version": 1, + "baseline_sha256": "d02dda330ac41ef05a1ff4e4d4f9ed04d5a60b24a444a28f1af9904791977799", + "current_sha256": "d02dda330ac41ef05a1ff4e4d4f9ed04d5a60b24a444a28f1af9904791977799" + }, + { + "operation": "studies.candidates", + "version": 1, + "baseline_sha256": "84eacef1aacffdfcba18ce037f013034ac959f23a617c8b443247c72f89fcfd2", + "current_sha256": "84eacef1aacffdfcba18ce037f013034ac959f23a617c8b443247c72f89fcfd2" + }, + { + "operation": "studies.prepare_package", + "version": 1, + "baseline_sha256": "6eea96340355a085a4709c180f3ae61bed063a344c3e4ad8b876c53f36ff6f14", + "current_sha256": "6eea96340355a085a4709c180f3ae61bed063a344c3e4ad8b876c53f36ff6f14" + }, + { + "operation": "studies.prepare_verifier", + "version": 1, + "baseline_sha256": "cde685bd4e93bb749055bb291501c67520f97dc514b2a4e4c3ba2cd1a10a6528", + "current_sha256": "cde685bd4e93bb749055bb291501c67520f97dc514b2a4e4c3ba2cd1a10a6528" + }, + { + "operation": "studies.package_state", + "version": 1, + "baseline_sha256": "4ad0fd6c580c794ed01a8f94fc9f0ee1e07b09578be940bc02be3581f44449bd", + "current_sha256": "4ad0fd6c580c794ed01a8f94fc9f0ee1e07b09578be940bc02be3581f44449bd" + }, + { + "operation": "runs.model_evidence", + "version": 1, + "baseline_sha256": "50c054fc7ce64c08f8a229faed6dfdcd9069c1b84ee8985bfce8736bc2c9aadb", + "current_sha256": "50c054fc7ce64c08f8a229faed6dfdcd9069c1b84ee8985bfce8736bc2c9aadb" + }, + { + "operation": "runs.model_evidence", + "version": 2, + "baseline_sha256": "48c1b942f8e7a102a83bf63806fb425576690f4bfbca841203d54edbca4a59f9", + "current_sha256": "48c1b942f8e7a102a83bf63806fb425576690f4bfbca841203d54edbca4a59f9" + }, + { + "operation": "runs.retained_analysis", + "version": 1, + "baseline_sha256": "8ec28793a0603b206cb720d0ece573793c474fed901fb8ce99133d0154526cd5", + "current_sha256": "8ec28793a0603b206cb720d0ece573793c474fed901fb8ce99133d0154526cd5" + }, + { + "operation": "runs.export_retained_analysis", + "version": 1, + "baseline_sha256": "0994932e96aab5d9561a63e3447bb8ec1f6345607e247ddb5a9cadaccffc57fb", + "current_sha256": "0994932e96aab5d9561a63e3447bb8ec1f6345607e247ddb5a9cadaccffc57fb" + }, + { + "operation": "runs.compare_model_evidence", + "version": 1, + "baseline_sha256": "75842ef0768d8b95ca6057df3f01e31b78659ff91f91ca3a49f3194d7031ebdd", + "current_sha256": "75842ef0768d8b95ca6057df3f01e31b78659ff91f91ca3a49f3194d7031ebdd" + }, + { + "operation": "runs.compare_model_evidence", + "version": 2, + "baseline_sha256": "0c89402e1749b5bca7c09c66bc5bfc13cd33b3be6e15c69826074583fc0e13c5", + "current_sha256": "0c89402e1749b5bca7c09c66bc5bfc13cd33b3be6e15c69826074583fc0e13c5" + }, + { + "operation": "project_snapshots.list", + "version": 1, + "baseline_sha256": "70ded61c792052ee0cf68e027fe9446d3a3758003f0259b240f9524eba7a6861", + "current_sha256": "70ded61c792052ee0cf68e027fe9446d3a3758003f0259b240f9524eba7a6861" + }, + { + "operation": "project_snapshots.get", + "version": 1, + "baseline_sha256": "b05a939114021af7a5dcb78a8ba2cb7eb879f9e12fc23d6f757f12796f19e153", + "current_sha256": "b05a939114021af7a5dcb78a8ba2cb7eb879f9e12fc23d6f757f12796f19e153" + }, + { + "operation": "project_snapshots.get", + "version": 2, + "baseline_sha256": "770e999697f069af13e9cf0d48daaf930ee26db4d7415169edecb414f68bdd01", + "current_sha256": "770e999697f069af13e9cf0d48daaf930ee26db4d7415169edecb414f68bdd01" + }, + { + "operation": "project_snapshots.open", + "version": 1, + "baseline_sha256": "a4be5809c583caf12641c889e733c8be41e5615bcc167c3414ab7a29ee16e036", + "current_sha256": "a4be5809c583caf12641c889e733c8be41e5615bcc167c3414ab7a29ee16e036" + }, + { + "operation": "project_snapshots.open", + "version": 2, + "baseline_sha256": "d65c3f5d499fec89d42f1251ca90a7471e13283f377c3f448015677f4a926089", + "current_sha256": "d65c3f5d499fec89d42f1251ca90a7471e13283f377c3f448015677f4a926089" + }, + { + "operation": "studies.lineage", + "version": 1, + "baseline_sha256": "33ee4e76a92b1481c2e6fca72b571b2f363ca18d9098acf065d025c2e27963f9", + "current_sha256": "33ee4e76a92b1481c2e6fca72b571b2f363ca18d9098acf065d025c2e27963f9" + }, + { + "operation": "studies.lineage_trial", + "version": 1, + "baseline_sha256": "f3ec663dfa413eeeaf5ab017c28d02d0ec4f14a724c0067d07592110bf7e694a", + "current_sha256": "f3ec663dfa413eeeaf5ab017c28d02d0ec4f14a724c0067d07592110bf7e694a" + }, + { + "operation": "studies.record_lineage", + "version": 1, + "baseline_sha256": "f8315af375e0667d4dd55e6d7decf8155ef70eaed2c19b60d7256010da9d26a4", + "current_sha256": "f8315af375e0667d4dd55e6d7decf8155ef70eaed2c19b60d7256010da9d26a4" + }, + { + "operation": "studies.branch_trial", + "version": 1, + "baseline_sha256": "76b6415bce7e03aef825e0a64fac690f6f2e46bcbb54fd3423979c87449b2af5", + "current_sha256": "76b6415bce7e03aef825e0a64fac690f6f2e46bcbb54fd3423979c87449b2af5" + }, + { + "operation": "studies.prepare_branch", + "version": 1, + "baseline_sha256": "7f4e4d8b92d86fc6f5dedf363c4ad4dbc125df914af9a18450aab0c894a13b43", + "current_sha256": "7f4e4d8b92d86fc6f5dedf363c4ad4dbc125df914af9a18450aab0c894a13b43" + }, + { + "operation": "runs.publication_status", + "version": 1, + "baseline_sha256": "f0ed22c9fcbcd55d4be475ca0c9fad9449d0f3b9aaf1662db084bd55f8eaef9c", + "current_sha256": "f0ed22c9fcbcd55d4be475ca0c9fad9449d0f3b9aaf1662db084bd55f8eaef9c" + }, + { + "operation": "runs.publication_status", + "version": 2, + "baseline_sha256": "d26adbe3218a97719fe36613bec4bf9f8fd021bac5bfeb52caa2bcfe4760f7fe", + "current_sha256": "d26adbe3218a97719fe36613bec4bf9f8fd021bac5bfeb52caa2bcfe4760f7fe" + }, + { + "operation": "runs.recover_publication", + "version": 1, + "baseline_sha256": "5a8f6c1dd91abf16ab00b0b7b31a6532d265bfbfc481af2a295d8370df5575f9", + "current_sha256": "5a8f6c1dd91abf16ab00b0b7b31a6532d265bfbfc481af2a295d8370df5575f9" + }, + { + "operation": "runs.cancel_publication", + "version": 1, + "baseline_sha256": "04767a30954a7de03de27d5971400120f0f772d8ce70ad88690c38e6890fdf4a", + "current_sha256": "04767a30954a7de03de27d5971400120f0f772d8ce70ad88690c38e6890fdf4a" + }, + { + "operation": "runs.phase_timings", + "version": 1, + "baseline_sha256": "2ccaba59a34e24203fdb873713e3607e4df8c5a942d1da508792b765a00e5da2", + "current_sha256": "2ccaba59a34e24203fdb873713e3607e4df8c5a942d1da508792b765a00e5da2" + }, + { + "operation": "graphs.preparation_timings", + "version": 1, + "baseline_sha256": "94f848f833254da60a4843ca4f943e1c4fcc62d036c7be2f2b44731fb559fd17", + "current_sha256": "94f848f833254da60a4843ca4f943e1c4fcc62d036c7be2f2b44731fb559fd17" + }, + { + "operation": "runs.coordinator_health", + "version": 1, + "baseline_sha256": "a5d87e0cf67706641dc361c2d783fdb1557dd2c1198f0db2506726a132b8cae9", + "current_sha256": "a5d87e0cf67706641dc361c2d783fdb1557dd2c1198f0db2506726a132b8cae9" + }, + { + "operation": "runs.recover_coordinator", + "version": 1, + "baseline_sha256": "d31aee53400b2f00adedb958feb054f636b9efd7e2e1a0afad0fd55a9fd46b19", + "current_sha256": "d31aee53400b2f00adedb958feb054f636b9efd7e2e1a0afad0fd55a9fd46b19" + }, + { + "operation": "runs.publication_result", + "version": 1, + "baseline_sha256": "fcdee230c0a3f03c6f6bea9a955617368d7ea18678d3da0b31df787e69f9cb17", + "current_sha256": "fcdee230c0a3f03c6f6bea9a955617368d7ea18678d3da0b31df787e69f9cb17" + }, + { + "operation": "runs.publication_artifact_chunk", + "version": 1, + "baseline_sha256": "6cdbc3fa18b569558918eab1672386546d05c70e98bf31f2694ece8d889bf9d2", + "current_sha256": "6cdbc3fa18b569558918eab1672386546d05c70e98bf31f2694ece8d889bf9d2" + }, + { + "operation": "runs.artifact_segments", + "version": 1, + "baseline_sha256": "510198cf8617f297b45b5b5100692e76aa3de5f865019730843b18d1d8ee7e39", + "current_sha256": "510198cf8617f297b45b5b5100692e76aa3de5f865019730843b18d1d8ee7e39" + }, + { + "operation": "runs.publication_model_evidence", + "version": 1, + "baseline_sha256": "d2f34f0df4d54b82bf4f52508de405f6d743d404c78648d6c4c19c65e7e4f1d2", + "current_sha256": "d2f34f0df4d54b82bf4f52508de405f6d743d404c78648d6c4c19c65e7e4f1d2" + }, + { + "operation": "runs.compare_publication_evidence", + "version": 1, + "baseline_sha256": "96796fa1b5c32f1106962cb0ec36aada7ff6d92c057870997e9a1b7f364b74bd", + "current_sha256": "96796fa1b5c32f1106962cb0ec36aada7ff6d92c057870997e9a1b7f364b74bd" + }, + { + "operation": "runs.compare_scientific_evidence", + "version": 1, + "baseline_sha256": "32fe4994c2a38f8d38335c3c2d1db84c82c0f3b61c6a11d95e71cc5f02f35be5", + "current_sha256": "32fe4994c2a38f8d38335c3c2d1db84c82c0f3b61c6a11d95e71cc5f02f35be5" + } + ] +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/final-source.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/final-source.json new file mode 100644 index 00000000..76253bc0 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/final-source.json @@ -0,0 +1,11 @@ +{ + "base": "1442f1849c5894500d69823e60bfcd3d644dc683", + "branch": "feature/material-preflight-reports-20261020", + "files": { + "pipeline.py": "5c879c741e7602c31cf1625d9673fc7464f5cef5c12554381a1ab662ccddca6d", + "agent_api.py": "b2c11bb56dd7688850cbfce5dc6445210056fbd7c966481e614e9eeb15a397c8", + "app.py": "0c8dc4da95fc803136ad2731a2e643e0fd14839535d740ef0d263ac117f9d276", + "tests/test_material_preflight_reports.py": "a8e43084b6b5145bfb5cd24b384cea3e3afba147b8ac1e4aa8c15ffe04af580c" + }, + "change_since_75_case_regression": "Only new material test fixture isolation, actual live CLI refusal, headerless refusal and inertness counts; production code unchanged." +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/independent-review.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/independent-review.json new file mode 100644 index 00000000..714d260c --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/independent-review.json @@ -0,0 +1,134 @@ +{ + "reviewed_utc": "2026-10-20T18:18:39.625818+00:00", + "disposition": "clear_for_local_source_checkpoint_with_stated_scope_limits", + "base": "1442f1849c5894500d69823e60bfcd3d644dc683", + "branch": "feature/material-preflight-reports-20261020", + "reviewed_state": "uncommitted isolated local dependent source; no publication or hosted acceptance inferred", + "reviewer_actions": "Source/test/docs/ledger review and local evidence hashing only. No test/import/PG query, repository edit or remote action.", + "source_hashes": { + "pipeline.py": "5c879c741e7602c31cf1625d9673fc7464f5cef5c12554381a1ab662ccddca6d", + "agent_api.py": "b2c11bb56dd7688850cbfce5dc6445210056fbd7c966481e614e9eeb15a397c8", + "app.py": "0c8dc4da95fc803136ad2731a2e643e0fd14839535d740ef0d263ac117f9d276", + "tests/test_material_preflight_reports.py": "a8e43084b6b5145bfb5cd24b384cea3e3afba147b8ac1e4aa8c15ffe04af580c" + }, + "canonical_docs_sha256": { + "docs/contracts/run-preflight-publication.md": "471862b521514b2b07eba3c61df0e44c0354b3833aee4477c95443b514e1ac5c", + "docs/contracts/agent-operations.md": "6fecd23d55f9bce890b740dea0f65875645575f13606b31d09e2b173f1a79ad7", + "docs/contracts/project-cli.md": "63d9552e80d73bb913c2359778e7f24a7669bd7abfb4e04cae89ccbfefd0297c", + "docs/plans/lab-workflow-reliability/execution-ledger.md": "049c5d190fc74ce9ecd2034c41abf1bbaf5e8b087ddf288ec031bd455b984984" + }, + "evidence_sha256": { + "source-for-regression-tests.json": "f8b08ef67ffbb4f3e9fb7ed11cc90a633ed56d819dc40c800c90db3dfb3ba6f1", + "source-for-final-tests.json": "a73db2e5c0ab678ae4cef8d0391fdb7cb5e38d79ccd8f295602a39b379ab5fed", + "regression-result.json": "7c016eaa8d2ca1f398c9c0a770d6457d90b9f3f3d5c219893996e2db1e419f47", + "material-followup-result.json": "d0a141c09655e4e97d892f0c815b8bff053490929dfc6377b59287e96b465b41", + "focused-attempt-01.log": "a0076a14aa735c4a9f89137b8421388d641e0995d1944ec335a4544e80005f96", + "material-followup.log": "661f410c3848de2cf2f650cb5191b5bd8edcf2285d7c1350b4e262d818610df5", + "all-prior-contracts.json": "b3feb141cec0346b635ae06306d794c99064bf250b9c3d75d7293f5f80290a44", + "run-focused.py": "9f684c68b8a33cd90fb749c2a07143606edaf315cc5aee6cba5ae361a90211d1", + "run-material-followup.py": "92bc771936069a605ba246a54dedefecd1844e274cdba9c75da182d2aa4ff41a" + }, + "bindings": { + "all_final_four_files_match_manifest": true, + "production_three_files_unchanged_between_runs": true, + "regression_test_sha256": "2b1ef0fd541d3f30b1db5fada7718dbb8ea5b3a385c9ec12038c162907707eaf", + "final_test_sha256": "a8e43084b6b5145bfb5cd24b384cea3e3afba147b8ac1e4aa8c15ffe04af580c" + }, + "tests": [ + { + "tests": 75, + "unittest_seconds": 246.263, + "wrapper_utc_seconds": 246.643246, + "failures": 0, + "errors": 0, + "skipped": 0, + "source_manifest": "source-for-regression-tests.json", + "result_file": "regression-result.json", + "raw_log": "focused-attempt-01.log", + "owned_schemas_removed_as_reported": [ + "catalog_1937048d1ac54ee8b748380c19a9fb16", + "catalog_4c40d8d1c09a45fab9fea7f0def45479", + "catalog_57796e8fbbc042a0b4f5a8144d5d3c53", + "catalog_6bea37acaf5c4744aea607d2322ed54e", + "catalog_8240f4833fef4881883718fac883e839", + "catalog_96a28c51b1a44e57b0c2201fa17d3b39", + "catalog_d491e85396da430cadc57c6faf285117", + "catalog_f36b38b44a804167a349f2a8e4bbc1a6" + ] + }, + { + "tests": 9, + "unittest_seconds": 20.883, + "wrapper_utc_seconds": 21.058332, + "failures": 0, + "errors": 0, + "skipped": 0, + "source_manifest": "source-for-final-tests.json", + "result_file": "material-followup-result.json", + "raw_log": "material-followup.log", + "owned_schemas_removed_as_reported": [ + "catalog_d4774151cd4a4541b5cb7243b53bb68b", + "catalog_dbd310cf502344c19f0c0145ad97e420" + ] + } + ], + "compatibility": { + "prior_contracts": 127, + "all_recorded_baseline_current_hash_pairs_equal": true, + "legacy_projection_exact": true, + "only_addition": [ + { + "operation": "recipes.preflight_material", + "version": 1 + } + ], + "limit": "Root-generated comparison receipt inspected; no independent module import/recomputation." + }, + "source_findings": [ + "Inspect-only recipes.preflight_material/1 reuses existing retained material/Model/capture verification and current external preparation, then rechecks current disclosure.", + "Builtin refuses before native preparation/probe; default internal Study path, cold recipes.material and durable Trial receipts/replay preserved.", + "Three digest meanings distinct: outer composition material SHA, inner execution_material_sha256, graph execution_sha256; exact request Ref/catalog correlation enforced.", + "Refusals preserve original typed status/code with null graph/model/material/execution proofs and bounded eleven-row request-only provenance. Early auth/malformed/foreign/missing/unclassified remain bare.", + "Closed schema/content hash, external-only trainer, same-catalog nested refs, runtime content identity and directly available evidence links enforced; final rehashed negative cases isolated." + ], + "nits_disposition": { + "nested_ref_and_runtime_evidence_forgery": "Fixed in frozen source with final independent rehashed negative cases.", + "graph_execution_hash_limit": "Explicitly documented owner-attested provenance; not independently reconstructible from outer projection.", + "credential_scan_wording": "Now explicitly limited to refusal reports.", + "actual_cli_path": "Final actual Study-server refusal uses project_cli and real AgentClient transport; only from_config returns existing real client.", + "test_aliasing": "Final fixture deepcopy prevents cross-negative mutation; final nine cases bind revised test SHA.", + "duration_precision": "Regression unittest246.263s differs from wrapper246.643246s; both retained separately." + }, + "coverage_limits": [ + "Actual disposable-PG fixture verifies retained revision after newer draft, real source drift, cold read and Trial replay, raw headerless409/auth refusal before owner, no-Popen tripwire and six unchanged domain-table counts.", + "Late disclosure refusal is injected AccessError on second owner read, not actual credential revocation. Capture tamper and source drift are real file changes.", + "Schema removal/absence is root-driver assertion evidence; reviewer made no independent PG query.", + "Source/host/output/graph hashes remain owner-attested in bounded projection; content hashes do not establish scientific truth.", + "No native builtin/Torch training/CUDA/scientific, admission/storage/device reservation, publication, full C1/T1/M1, PUB-1 or SCI-1 qualification.", + "Historical initial harness/import errors remain separate; no future qualification/report file not yet reviewed is certified." + ], + "blockers": [], + "authored_report_review": { + "status": "final acceptance and qualification wording clear within explicit external-only scope", + "sha256": { + "acceptance.md": "c5c591259022a0176ad517f0b0983c28c6a6b1151c843d3246f18cc45c15cb4c", + "qualification.json": "d8479d60b7e379ab0d2b875a963687022ed810a144c8d2f081a569f7ec01b863", + "preservation.json": "ab16ba8f2e89fdd4e4c861dace7d92e4c93b7ec34cb3fcb7dc3217c0b215040a" + }, + "all_ten_archived_evidence_hashes_match": true, + "source_checkpoint": "Base1442f184 plus final four byte hashes identify reviewed subject; future containing commit not yet created.", + "early_attempt_fidelity": "Early harness errors/pure skips explicitly retained as historical summary, distinct from75 and9 qualified runs; raw early error logs were not independently inspected.", + "cycle_avoidance": "Qualification references review filename, not review hash; receipt binds final qualification hash." + }, + "independent_local_preservation": { + "all_97_prior_local_heads_exact": true, + "all_111_prior_worktree_records_exactly_present": true, + "only_added_local_head": [ + "refs/heads/feature/material-preflight-reports-20261020" + ], + "only_added_worktree_records": 1, + "remote_preservation": "No remote reads performed." + }, + "finalized_utc": "2026-10-20T18:19:48.626893+00:00", + "limitations_on_clearance": "No remaining local source blocker. This is clearance for exact local source checkpoint only; no remote publication, hosted qualification, full C1/T1/M1/science/hardware or remaining PUB-1/SCI-1 acceptance." +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-result.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-result.json new file mode 100644 index 00000000..aa1e7929 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-result.json @@ -0,0 +1,14 @@ +{ + "started_utc": "2026-10-20T18:16:41.275819+00:00", + "completed_utc": "2026-10-20T18:17:02.334151+00:00", + "tests": 9, + "failures": 0, + "errors": 0, + "skipped": 0, + "owned_schemas_removed": [ + "catalog_d4774151cd4a4541b5cb7243b53bb68b", + "catalog_dbd310cf502344c19f0c0145ad97e420" + ], + "no_constructor_or_domain_substitution": "Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.", + "source_tree": "uncommitted isolated dependent branch from1442f184; exactsourcehashmanifest retained separately" +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-runner.py b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-runner.py new file mode 100644 index 00000000..afab0989 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup-runner.py @@ -0,0 +1,40 @@ +from pathlib import Path +import datetime,json,os,sys,unittest +from unittest.mock import patch +import psycopg +from psycopg import sql +root=Path('/workspace/Tuldok-material-preflight-reports-20261020');sys.path[:0]=[str(root/'tests'),str(root)] +out=Path('/tmp/tuldok-material-preflight-reports-20261020') +import test_extension_bindings as bindings +import test_large_recipe_contract as large +import test_agent_project_snapshots as snapshots +import test_external_runs as external +import test_experiments as studies +owned=set() +with psycopg.connect(os.environ['TULDOK_DATABASE_URL'],autocommit=True) as connection: + before={r[0]for r in connection.execute("select nspname from pg_namespace where nspname like 'catalog_%'")} + def tracked(original): + def setup(case): + original(case);schema=case.dataset.db.schema;assert schema not in before + owned.add(schema);print(json.dumps({'fixture_owner':type(case).__name__,'new_schema':schema}),flush=True) + return setup + loader=unittest.TestLoader();suite=unittest.TestSuite() + suite.addTests(loader.loadTestsFromName('test_material_preflight_reports')) + started=datetime.datetime.now(datetime.timezone.utc).isoformat() + try: + with patch.object(external.ExternalCase,'setUp',tracked(external.ExternalCase.setUp)), \ + patch.object(bindings.BoundExtensionRecipeTests,'setUp',tracked(bindings.BoundExtensionRecipeTests.setUp)), \ + patch.object(large.LargeRecipeContract,'setUp',tracked(large.LargeRecipeContract.setUp)): + result=unittest.TextTestRunner(verbosity=2).run(suite) + finally: + for schema in sorted(owned): + assert schema not in before + connection.execute(sql.SQL('DROP SCHEMA {} CASCADE').format(sql.Identifier(schema))) + assert connection.execute('select 1 from pg_namespace where nspname=%s',(schema,)).fetchone()is None + print(json.dumps({'removed_exact_owned_schema':schema}),flush=True) + receipt={'started_utc':started,'completed_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(), + 'tests':result.testsRun,'failures':len(result.failures),'errors':len(result.errors),'skipped':len(result.skipped), + 'owned_schemas_removed':sorted(owned),'no_constructor_or_domain_substitution':'Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.', + 'source_tree':'uncommitted isolated dependent branch from1442f184; exactsourcehashmanifest retained separately'} + (out/'material-followup-result.json').write_text(json.dumps(receipt,indent=2)+'\n');print(json.dumps(receipt),flush=True) +raise SystemExit(not result.wasSuccessful()) diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup.log b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup.log new file mode 100644 index 00000000..2ba13aa6 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/material-followup.log @@ -0,0 +1,19 @@ +test_actual_study_material_http_cli_drift_cold_read_and_replay_are_inert (test_material_preflight_reports.MaterialPreflightHTTPTests.test_actual_study_material_http_cli_drift_cold_read_and_replay_are_inert) ... {"fixture_owner": "StudyTests", "new_schema": "catalog_d4774151cd4a4541b5cb7243b53bb68b"} +ok +test_current_disclosure_rechecked_and_retained_tamper_has_unknown_proofs (test_material_preflight_reports.MaterialPreflightHTTPTests.test_current_disclosure_rechecked_and_retained_tamper_has_unknown_proofs) ... {"fixture_owner": "StudyTests", "new_schema": "catalog_dbd310cf502344c19f0c0145ad97e420"} +ok +test_builtin_refuses_before_probe_and_early_foreign_ref_stays_bare (test_material_preflight_reports.MaterialPreflightReportTests.test_builtin_refuses_before_probe_and_early_foreign_ref_stays_bare) ... ok +test_client_correlates_ref_catalog_and_real_error_transport_with_no_retry (test_material_preflight_reports.MaterialPreflightReportTests.test_client_correlates_ref_catalog_and_real_error_transport_with_no_retry) ... ok +test_consumer_refuses_rehashed_foreign_or_builtin_compositions_and_runtime_provenance (test_material_preflight_reports.MaterialPreflightReportTests.test_consumer_refuses_rehashed_foreign_or_builtin_compositions_and_runtime_provenance) ... ok +test_consumer_rejects_forged_links_proofs_owners_states_and_bounds (test_material_preflight_reports.MaterialPreflightReportTests.test_consumer_rejects_forged_links_proofs_owners_states_and_bounds) ... ok +test_default_internal_path_and_private_verified_ref_fence_are_preserved (test_material_preflight_reports.MaterialPreflightReportTests.test_default_internal_path_and_private_verified_ref_fence_are_preserved) ... ok +test_distinct_outer_inner_and_graph_identities_and_deferred_execution (test_material_preflight_reports.MaterialPreflightReportTests.test_distinct_outer_inner_and_graph_identities_and_deferred_execution) ... ok +test_refusal_is_bounded_request_provenance_with_unknown_identities (test_material_preflight_reports.MaterialPreflightReportTests.test_refusal_is_bounded_request_provenance_with_unknown_identities) ... ok + +---------------------------------------------------------------------- +Ran 9 tests in 20.883s + +OK +{"removed_exact_owned_schema": "catalog_d4774151cd4a4541b5cb7243b53bb68b"} +{"removed_exact_owned_schema": "catalog_dbd310cf502344c19f0c0145ad97e420"} +{"started_utc": "2026-10-20T18:16:41.275819+00:00", "completed_utc": "2026-10-20T18:17:02.334151+00:00", "tests": 9, "failures": 0, "errors": 0, "skipped": 0, "owned_schemas_removed": ["catalog_d4774151cd4a4541b5cb7243b53bb68b", "catalog_dbd310cf502344c19f0c0145ad97e420"], "no_constructor_or_domain_substitution": "Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.", "source_tree": "uncommitted isolated dependent branch from1442f184; exactsourcehashmanifest retained separately"} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/preservation.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/preservation.json new file mode 100644 index 00000000..fb1a7f77 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/preservation.json @@ -0,0 +1,10 @@ +{ + "original_local_heads": 97, + "all_original_heads_exact": true, + "original_worktrees": 111, + "all_original_worktrees_present": true, + "only_added_local_heads": [ + "refs/heads/feature/material-preflight-reports-20261020" + ], + "graph_checkpoint": "1442f1849c5894500d69823e60bfcd3d644dc683" +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/qualification.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/qualification.json new file mode 100644 index 00000000..2bfd2765 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/qualification.json @@ -0,0 +1,99 @@ +{ + "recorded_utc": "2026-10-20T18:18:09.251210+00:00", + "base": "1442f1849c5894500d69823e60bfcd3d644dc683", + "branch": "feature/material-preflight-reports-20261020", + "status": "scoped_external_material_reporting_accepted_locally", + "runs": [ + { + "log": "regression.log", + "result": "regression-result.json", + "source": "regression-source.json", + "tests": 75, + "seconds": 246.263, + "failures": 0, + "errors": 0, + "skipped": 0, + "owned_schemas_removed": 8 + }, + { + "log": "material-followup.log", + "result": "material-followup-result.json", + "source": "final-source.json", + "tests": 9, + "seconds": 20.883, + "failures": 0, + "errors": 0, + "skipped": 0, + "owned_schemas_removed": 2 + } + ], + "production_unchanged_between_runs": true, + "source_hashes": { + "pipeline.py": "5c879c741e7602c31cf1625d9673fc7464f5cef5c12554381a1ab662ccddca6d", + "agent_api.py": "b2c11bb56dd7688850cbfce5dc6445210056fbd7c966481e614e9eeb15a397c8", + "app.py": "0c8dc4da95fc803136ad2731a2e643e0fd14839535d740ef0d263ac117f9d276", + "tests/test_material_preflight_reports.py": "a8e43084b6b5145bfb5cd24b384cea3e3afba147b8ac1e4aa8c15ffe04af580c" + }, + "compatibility": { + "prior_contracts": 127, + "all_hashes_exact": true, + "legacy_projection_exact": true, + "only_added": "recipes.preflight_material/1" + }, + "preservation": { + "original_local_heads": 97, + "all_original_heads_exact": true, + "original_worktrees": 111, + "all_original_worktrees_present": true, + "only_added_local_heads": [ + "refs/heads/feature/material-preflight-reports-20261020" + ], + "graph_checkpoint": "1442f1849c5894500d69823e60bfcd3d644dc683" + }, + "early_attempts": [ + { + "tests": 8, + "errors": 2, + "skipped": 2, + "cause": "New test harness used mutation/required_scopes instead of side_effects/scopes and positional CLI operation instead of --operation." + }, + { + "tests": 8, + "errors": 1, + "skipped": 2, + "cause": "New CLI test supplied inline JSON where --arguments requires a regular JSON file." + }, + { + "tests": 9, + "errors": 0, + "failures": 0, + "skipped": 2, + "scope": "Pure/HTTP unit pass only; database acceptance awaited qualified run." + } + ], + "review_corrections": [ + "Foreign trainer/profile/runtime and builtin success compositions rejected; directly correlatable runtime/inner/output evidence hashes checked.", + "Graph execution digest documented as owner-attested in bounded outer-material projection.", + "Adversarial fixture deep-copied so independent rehashed negatives cannot inherit earlier foreign refs.", + "Final focused run adds live Study HTTP through CLI (only from_config supplies existing real client), headerless refusal and exact six-table inertness." + ], + "limitations": [ + "No hosted CI run/publication/merge selected.", + "No native builtin runtime or numerical/hardware/scientific acceptance claimed.", + "Late disclosure loss is an injected AccessError over actual retained material; malformed/auth/headerless/source-drift/tamper outcomes exercise their actual owners.", + "No PUB-1 failure input or SCI-1 scientific pair/criterion/environment supplied; full C1/T1/M1 remains open." + ], + "evidence_sha256": { + "regression.log": "a0076a14aa735c4a9f89137b8421388d641e0995d1944ec335a4544e80005f96", + "material-followup.log": "661f410c3848de2cf2f650cb5191b5bd8edcf2285d7c1350b4e262d818610df5", + "regression-source.json": "f8b08ef67ffbb4f3e9fb7ed11cc90a633ed56d819dc40c800c90db3dfb3ba6f1", + "final-source.json": "a73db2e5c0ab678ae4cef8d0391fdb7cb5e38d79ccd8f295602a39b379ab5fed", + "all-prior-contracts.json": "b3feb141cec0346b635ae06306d794c99064bf250b9c3d75d7293f5f80290a44", + "regression-result.json": "7c016eaa8d2ca1f398c9c0a770d6457d90b9f3f3d5c219893996e2db1e419f47", + "material-followup-result.json": "d0a141c09655e4e97d892f0c815b8bff053490929dfc6377b59287e96b465b41", + "regression-runner.py": "9f684c68b8a33cd90fb749c2a07143606edaf315cc5aee6cba5ae361a90211d1", + "material-followup-runner.py": "92bc771936069a605ba246a54dedefecd1844e274cdba9c75da182d2aa4ff41a", + "preservation.json": "ab16ba8f2e89fdd4e4c861dace7d92e4c93b7ec34cb3fcb7dc3217c0b215040a" + }, + "independent_review": "independent-review.json; separately binds final source, both test subjects, compatibility and this acceptance evidence without a circular receipt hash" +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-result.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-result.json new file mode 100644 index 00000000..beadd187 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-result.json @@ -0,0 +1,20 @@ +{ + "started_utc": "2026-10-20T18:09:14.114818+00:00", + "completed_utc": "2026-10-20T18:13:20.758064+00:00", + "tests": 75, + "failures": 0, + "errors": 0, + "skipped": 0, + "owned_schemas_removed": [ + "catalog_1937048d1ac54ee8b748380c19a9fb16", + "catalog_4c40d8d1c09a45fab9fea7f0def45479", + "catalog_57796e8fbbc042a0b4f5a8144d5d3c53", + "catalog_6bea37acaf5c4744aea607d2322ed54e", + "catalog_8240f4833fef4881883718fac883e839", + "catalog_96a28c51b1a44e57b0c2201fa17d3b39", + "catalog_d491e85396da430cadc57c6faf285117", + "catalog_f36b38b44a804167a349f2a8e4bbc1a6" + ], + "no_constructor_or_domain_substitution": "Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.", + "source_tree": "uncommitted isolated dependent branch from1442f184; exactsourcehashmanifest retained separately" +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-runner.py b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-runner.py new file mode 100644 index 00000000..cbfd09f1 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-runner.py @@ -0,0 +1,45 @@ +from pathlib import Path +import datetime,json,os,sys,unittest +from unittest.mock import patch +import psycopg +from psycopg import sql +root=Path('/workspace/Tuldok-material-preflight-reports-20261020');sys.path[:0]=[str(root/'tests'),str(root)] +out=Path('/tmp/tuldok-material-preflight-reports-20261020') +import test_extension_bindings as bindings +import test_large_recipe_contract as large +import test_agent_project_snapshots as snapshots +import test_external_runs as external +import test_experiments as studies +owned=set() +with psycopg.connect(os.environ['TULDOK_DATABASE_URL'],autocommit=True) as connection: + before={r[0]for r in connection.execute("select nspname from pg_namespace where nspname like 'catalog_%'")} + def tracked(original): + def setup(case): + original(case);schema=case.dataset.db.schema;assert schema not in before + owned.add(schema);print(json.dumps({'fixture_owner':type(case).__name__,'new_schema':schema}),flush=True) + return setup + loader=unittest.TestLoader();suite=unittest.TestSuite() + for module in ['test_material_preflight_reports','test_run_preflight_refusals','test_run_preflight_report','test_run_preflight_storage', + 'test_workflow_client','test_agent_project_cli']: + suite.addTests(loader.loadTestsFromName(module)) + suite.addTests(loader.loadTestsFromTestCase(snapshots.SnapshotRegistryTests)) + suite.addTest(snapshots.AgentSnapshotHTTPTests('test_agent_graph_preflight_and_trial_preparation_are_inert_under_owner_budget')) + suite.addTest(studies.StudyTests('test_preparation_is_inert_atomic_and_reconciles_after_draft_edit')) + started=datetime.datetime.now(datetime.timezone.utc).isoformat() + try: + with patch.object(external.ExternalCase,'setUp',tracked(external.ExternalCase.setUp)), \ + patch.object(bindings.BoundExtensionRecipeTests,'setUp',tracked(bindings.BoundExtensionRecipeTests.setUp)), \ + patch.object(large.LargeRecipeContract,'setUp',tracked(large.LargeRecipeContract.setUp)): + result=unittest.TextTestRunner(verbosity=2).run(suite) + finally: + for schema in sorted(owned): + assert schema not in before + connection.execute(sql.SQL('DROP SCHEMA {} CASCADE').format(sql.Identifier(schema))) + assert connection.execute('select 1 from pg_namespace where nspname=%s',(schema,)).fetchone()is None + print(json.dumps({'removed_exact_owned_schema':schema}),flush=True) + receipt={'started_utc':started,'completed_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(), + 'tests':result.testsRun,'failures':len(result.failures),'errors':len(result.errors),'skipped':len(result.skipped), + 'owned_schemas_removed':sorted(owned),'no_constructor_or_domain_substitution':'Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.', + 'source_tree':'uncommitted isolated dependent branch from1442f184; exactsourcehashmanifest retained separately'} + (out/'focused-result.json').write_text(json.dumps(receipt,indent=2)+'\n');print(json.dumps(receipt),flush=True) +raise SystemExit(not result.wasSuccessful()) diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-source.json b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-source.json new file mode 100644 index 00000000..d3530935 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression-source.json @@ -0,0 +1,10 @@ +{ + "base": "1442f1849c5894500d69823e60bfcd3d644dc683", + "branch": "feature/material-preflight-reports-20261020", + "files": { + "pipeline.py": "5c879c741e7602c31cf1625d9673fc7464f5cef5c12554381a1ab662ccddca6d", + "agent_api.py": "b2c11bb56dd7688850cbfce5dc6445210056fbd7c966481e614e9eeb15a397c8", + "app.py": "0c8dc4da95fc803136ad2731a2e643e0fd14839535d740ef0d263ac117f9d276", + "tests/test_material_preflight_reports.py": "2b1ef0fd541d3f30b1db5fada7718dbb8ea5b3a385c9ec12038c162907707eaf" + } +} diff --git a/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression.log b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression.log new file mode 100644 index 00000000..81686d77 --- /dev/null +++ b/docs/plans/lab-workflow-reliability/reports/material-preflight-reports-20261020/regression.log @@ -0,0 +1,98 @@ +test_actual_study_material_http_cli_drift_cold_read_and_replay_are_inert (test_material_preflight_reports.MaterialPreflightHTTPTests.test_actual_study_material_http_cli_drift_cold_read_and_replay_are_inert) ... {"fixture_owner": "StudyTests", "new_schema": "catalog_4c40d8d1c09a45fab9fea7f0def45479"} +ok +test_current_disclosure_rechecked_and_retained_tamper_has_unknown_proofs (test_material_preflight_reports.MaterialPreflightHTTPTests.test_current_disclosure_rechecked_and_retained_tamper_has_unknown_proofs) ... {"fixture_owner": "StudyTests", "new_schema": "catalog_f36b38b44a804167a349f2a8e4bbc1a6"} +ok +test_builtin_refuses_before_probe_and_early_foreign_ref_stays_bare (test_material_preflight_reports.MaterialPreflightReportTests.test_builtin_refuses_before_probe_and_early_foreign_ref_stays_bare) ... ok +test_client_correlates_ref_catalog_and_real_error_transport_with_no_retry (test_material_preflight_reports.MaterialPreflightReportTests.test_client_correlates_ref_catalog_and_real_error_transport_with_no_retry) ... ok +test_consumer_refuses_rehashed_foreign_or_builtin_compositions_and_runtime_provenance (test_material_preflight_reports.MaterialPreflightReportTests.test_consumer_refuses_rehashed_foreign_or_builtin_compositions_and_runtime_provenance) ... ok +test_consumer_rejects_forged_links_proofs_owners_states_and_bounds (test_material_preflight_reports.MaterialPreflightReportTests.test_consumer_rejects_forged_links_proofs_owners_states_and_bounds) ... ok +test_default_internal_path_and_private_verified_ref_fence_are_preserved (test_material_preflight_reports.MaterialPreflightReportTests.test_default_internal_path_and_private_verified_ref_fence_are_preserved) ... ok +test_distinct_outer_inner_and_graph_identities_and_deferred_execution (test_material_preflight_reports.MaterialPreflightReportTests.test_distinct_outer_inner_and_graph_identities_and_deferred_execution) ... ok +test_refusal_is_bounded_request_provenance_with_unknown_identities (test_material_preflight_reports.MaterialPreflightReportTests.test_refusal_is_bounded_request_provenance_with_unknown_identities) ... ok +test_actual_owner_refusal_and_old_http_contracts_leave_execution_counts_unchanged (test_run_preflight_refusals.PreflightRefusalHTTPTests.test_actual_owner_refusal_and_old_http_contracts_leave_execution_counts_unchanged) ... {"fixture_owner": "BoundExtensionRecipeTests", "new_schema": "catalog_6bea37acaf5c4744aea607d2322ed54e"} +ok +test_authorization_and_early_refusals_have_no_report_or_extra_graph_read (test_run_preflight_refusals.PreflightRefusalTests.test_authorization_and_early_refusals_have_no_report_or_extra_graph_read) ... ok +test_client_correlates_error_report_without_retry_and_wrappers_retain_it (test_run_preflight_refusals.PreflightRefusalTests.test_client_correlates_error_report_without_retry_and_wrappers_retain_it) ... ok +test_consumers_reject_forged_proof_status_duplicates_bounds_and_hash (test_run_preflight_refusals.PreflightRefusalTests.test_consumers_reject_forged_proof_status_duplicates_bounds_and_hash) ... ok +test_domain_preserves_original_error_and_does_not_reprepare_or_publish (test_run_preflight_refusals.PreflightRefusalTests.test_domain_preserves_original_error_and_does_not_reprepare_or_publish) ... ok +test_installed_contract_hashes_and_legacy_projection_are_unchanged (test_run_preflight_refusals.PreflightRefusalTests.test_installed_contract_hashes_and_legacy_projection_are_unchanged) ... ok +test_real_http_error_normalization_preserves_only_selected_report_and_redacts (test_run_preflight_refusals.PreflightRefusalTests.test_real_http_error_normalization_preserves_only_selected_report_and_redacts) ... ok +test_refusals_preserve_typed_outcome_and_only_request_provenance (test_run_preflight_refusals.PreflightRefusalTests.test_refusals_preserve_typed_outcome_and_only_request_provenance) ... ok +test_storage_owner_and_graph_drift_do_not_reuse_checked_material (test_run_preflight_refusals.PreflightRefusalTests.test_storage_owner_and_graph_drift_do_not_reuse_checked_material) ... ok +test_client_correlates_graph_revision_and_catalog_without_retry (test_run_preflight_report.PreflightReportTests.test_client_correlates_graph_revision_and_catalog_without_retry) ... ok +test_clone_or_revision_changes_report_without_claiming_new_execution (test_run_preflight_report.PreflightReportTests.test_clone_or_revision_changes_report_without_claiming_new_execution) ... ok +test_consumer_refuses_inconsistent_identities_even_with_repaired_report_hash (test_run_preflight_report.PreflightReportTests.test_consumer_refuses_inconsistent_identities_even_with_repaired_report_hash) ... ok +test_domain_calls_fresh_preparation_and_only_selected_v3_adds_report (test_run_preflight_report.PreflightReportTests.test_domain_calls_fresh_preparation_and_only_selected_v3_adds_report) ... ok +test_graph_drift_or_preparation_failure_cannot_emit_checked_report (test_run_preflight_report.PreflightReportTests.test_graph_drift_or_preparation_failure_cannot_emit_checked_report) ... ok +test_mutable_execution_facts_are_explicitly_deferred (test_run_preflight_report.PreflightReportTests.test_mutable_execution_facts_are_explicitly_deferred) ... ok +test_new_wire_contract_requires_closed_bounded_report_and_old_overlap_survives (test_run_preflight_report.PreflightReportTests.test_new_wire_contract_requires_closed_bounded_report_and_old_overlap_survives) ... ok +test_report_binds_exact_material_graph_and_its_own_content (test_run_preflight_report.PreflightReportTests.test_report_binds_exact_material_graph_and_its_own_content) ... ok +test_report_preserves_prepared_material_and_approval_identity (test_run_preflight_report.PreflightReportTests.test_report_preserves_prepared_material_and_approval_identity) ... ok +test_unsupported_report_versions_refuse_before_preparation (test_run_preflight_report.PreflightReportTests.test_unsupported_report_versions_refuse_before_preparation) ... ok +test_utf8_content_identity_is_distinct_from_ascii_wire_encoding (test_run_preflight_report.PreflightReportTests.test_utf8_content_identity_is_distinct_from_ascii_wire_encoding) ... ok +test_real_preparation_storage_refusal_has_no_admission_or_staging_effect (test_run_preflight_storage.PreflightStorageIntegrationTests.test_real_preparation_storage_refusal_has_no_admission_or_staging_effect) ... {"fixture_owner": "LargeRecipeContract", "new_schema": "catalog_1937048d1ac54ee8b748380c19a9fb16"} +ok +test_admission_keeps_existing_exact_policy_and_boundary (test_run_preflight_storage.PreflightStorageTests.test_admission_keeps_existing_exact_policy_and_boundary) ... ok +test_generated_model_context_uses_existing_context_capture_limit (test_run_preflight_storage.PreflightStorageTests.test_generated_model_context_uses_existing_context_capture_limit) ... ok +test_preflight_includes_capture_that_admission_already_staged (test_run_preflight_storage.PreflightStorageTests.test_preflight_includes_capture_that_admission_already_staged) ... ok +test_report_labels_current_observation_without_claiming_a_reservation (test_run_preflight_storage.PreflightStorageTests.test_report_labels_current_observation_without_claiming_a_reservation) ... ok +test_selected_v3_checks_storage_before_issuing_report_and_rechecks_graph (test_run_preflight_storage.PreflightStorageTests.test_selected_v3_checks_storage_before_issuing_report_and_rechecks_graph) ... ok +test_storage_observation_cannot_hide_graph_drift_or_missing_owner (test_run_preflight_storage.PreflightStorageTests.test_storage_observation_cannot_hide_graph_drift_or_missing_owner) ... ok +test_storage_refusal_cannot_emit_report_or_change_old_projections (test_run_preflight_storage.PreflightStorageTests.test_storage_refusal_cannot_emit_report_or_change_old_projections) ... ok +test_unknown_storage_refuses_and_other_profiles_keep_existing_behavior (test_run_preflight_storage.PreflightStorageTests.test_unknown_storage_refuses_and_other_profiles_keep_existing_behavior) ... ok +test_exact_v2_reply_accepts_requested_mode_and_ordered_operands (test_workflow_client.ComparisonReplyTests.test_exact_v2_reply_accepts_requested_mode_and_ordered_operands) ... ok +test_exact_v2_reply_rejects_schema_valid_wrong_mode_sides_and_hashes (test_workflow_client.ComparisonReplyTests.test_exact_v2_reply_rejects_schema_valid_wrong_mode_sides_and_hashes) ... ok +test_legacy_v1_reply_keeps_its_existing_wire_contract (test_workflow_client.ComparisonReplyTests.test_legacy_v1_reply_keeps_its_existing_wire_contract) ... ok +test_5xx_and_negative_reconciliation_preserve_original_unknown_identity (test_workflow_client.WorkflowClientTests.test_5xx_and_negative_reconciliation_preserve_original_unknown_identity) ... ok +test_cancel_reports_request_without_claiming_drain (test_workflow_client.WorkflowClientTests.test_cancel_reports_request_without_claiming_drain) ... ok +test_cancelled_resume_never_completes_from_unvisited_prior_proofs (test_workflow_client.WorkflowClientTests.test_cancelled_resume_never_completes_from_unvisited_prior_proofs) ... ok +test_changed_inventory_local_success_and_closed_manifests_refuse (test_workflow_client.WorkflowClientTests.test_changed_inventory_local_success_and_closed_manifests_refuse) ... ok +test_exact_comparison_refuses_v1_without_implicit_operand_fallback (test_workflow_client.WorkflowClientTests.test_exact_comparison_refuses_v1_without_implicit_operand_fallback) ... ok +test_failure_preserves_success_and_outstanding_without_alternate_dispatch (test_workflow_client.WorkflowClientTests.test_failure_preserves_success_and_outstanding_without_alternate_dispatch) ... ok +test_hash_mismatch_and_no_clobber_race_keep_partial_typed_and_remove_provisional (test_workflow_client.WorkflowClientTests.test_hash_mismatch_and_no_clobber_race_keep_partial_typed_and_remove_provisional) ... ok +test_import_is_stdlib_only_with_startup_tripwires_in_clean_python (test_workflow_client.WorkflowClientTests.test_import_is_stdlib_only_with_startup_tripwires_in_clean_python) ... ok +test_interrupted_segments_resume_at_zero_with_whole_hash_and_current_authority (test_workflow_client.WorkflowClientTests.test_interrupted_segments_resume_at_zero_with_whole_hash_and_current_authority) ... ok +test_material_digest_uses_domain_utf8_canonical_json_for_unicode (test_workflow_client.WorkflowClientTests.test_material_digest_uses_domain_utf8_canonical_json_for_unicode) ... ok +test_missing_selected_submission_capability_refuses_without_dispatch (test_workflow_client.WorkflowClientTests.test_missing_selected_submission_capability_refuses_without_dispatch) ... ok +test_monitor_deadline_and_stop_never_call_cancel_and_restore_transport_budget (test_workflow_client.WorkflowClientTests.test_monitor_deadline_and_stop_never_call_cancel_and_restore_transport_budget) ... ok +test_pre_stopped_collection_never_bootstraps_and_retains_prior_item_proofs (test_workflow_client.WorkflowClientTests.test_pre_stopped_collection_never_bootstraps_and_retains_prior_item_proofs) ... ok +test_public_inventory_can_include_unselectable_manifest_metadata (test_workflow_client.WorkflowClientTests.test_public_inventory_can_include_unselectable_manifest_metadata) ... ok +test_read_budget_refuses_mutation_and_cleans_thread_local_context (test_workflow_client.WorkflowClientTests.test_read_budget_refuses_mutation_and_cleans_thread_local_context) ... ok +test_resume_rejects_destination_replacement_during_whole_hash_verification (test_workflow_client.WorkflowClientTests.test_resume_rejects_destination_replacement_during_whole_hash_verification) ... ok +test_study_acknowledgement_matches_owner_request_parent_catalog_and_grant (test_workflow_client.WorkflowClientTests.test_study_acknowledgement_matches_owner_request_parent_catalog_and_grant) ... ok +test_unknown_submission_never_retries_and_explicit_reconcile_keeps_identity (test_workflow_client.WorkflowClientTests.test_unknown_submission_never_retries_and_explicit_reconcile_keeps_identity) ... ok +test_authentication_and_invalid_capabilities_never_select_legacy_dispatch (test_agent_project_cli.CommandSelectionTests.test_authentication_and_invalid_capabilities_never_select_legacy_dispatch) ... ok +test_invalid_command_arguments_refuse_before_capability_bootstrap (test_agent_project_cli.CommandSelectionTests.test_invalid_command_arguments_refuse_before_capability_bootstrap) ... ok +test_open_and_inspect_select_snapshots_or_published_legacy_operations_before_dispatch (test_agent_project_cli.CommandSelectionTests.test_open_and_inspect_select_snapshots_or_published_legacy_operations_before_dispatch) ... ok +test_selected_command_failure_never_retries_the_legacy_operation (test_agent_project_cli.CommandSelectionTests.test_selected_command_failure_never_retries_the_legacy_operation) ... ok +test_missing_grant_invalid_selection_and_stale_source_leave_no_run (test_agent_project_cli.ProjectCLIHTTPTests.test_missing_grant_invalid_selection_and_stale_source_leave_no_run) ... {"fixture_owner": "ProjectCLIHTTPTests", "new_schema": "catalog_d491e85396da430cadc57c6faf285117"} +ok +test_open_prepare_approve_run_and_download_share_real_saved_owners (test_agent_project_cli.ProjectCLIHTTPTests.test_open_prepare_approve_run_and_download_share_real_saved_owners) ... {"fixture_owner": "ProjectCLIHTTPTests", "new_schema": "catalog_57796e8fbbc042a0b4f5a8144d5d3c53"} +ok +test_argument_file_refuses_symlinks_duplicate_keys_nonfinite_and_fifo (test_agent_project_cli.PublicationTests.test_argument_file_refuses_symlinks_duplicate_keys_nonfinite_and_fifo) ... ok +test_concurrent_destination_creation_refuses_publication_without_overwriting (test_agent_project_cli.PublicationTests.test_concurrent_destination_creation_refuses_publication_without_overwriting) ... ok +test_corrupt_whole_bytes_and_refused_read_leave_no_published_or_partial_file (test_agent_project_cli.PublicationTests.test_corrupt_whole_bytes_and_refused_read_leave_no_published_or_partial_file) ... ok +test_existing_regular_and_dangling_symlink_destinations_are_preserved (test_agent_project_cli.PublicationTests.test_existing_regular_and_dangling_symlink_destinations_are_preserved) ... ok +test_valid_empty_and_nonempty_bytes_are_published_with_private_mode (test_agent_project_cli.PublicationTests.test_valid_empty_and_nonempty_bytes_are_published_with_private_mode) ... ok +test_all_published_main_contracts_keep_their_exact_identities_and_overlap (test_agent_project_snapshots.SnapshotRegistryTests.test_all_published_main_contracts_keep_their_exact_identities_and_overlap) ... ok +test_explicit_snapshot_contracts_preserve_the_installed_legacy_registry (test_agent_project_snapshots.SnapshotRegistryTests.test_explicit_snapshot_contracts_preserve_the_installed_legacy_registry) ... ok +test_snapshot_arguments_are_closed_and_bounded (test_agent_project_snapshots.SnapshotRegistryTests.test_snapshot_arguments_are_closed_and_bounded) ... ok +test_agent_graph_preflight_and_trial_preparation_are_inert_under_owner_budget (test_agent_project_snapshots.AgentSnapshotHTTPTests.test_agent_graph_preflight_and_trial_preparation_are_inert_under_owner_budget) +Tiny non-runnable runtime bytes prove static capture, never CPU execution. ... {"fixture_owner": "BoundExtensionRecipeTests", "new_schema": "catalog_8240f4833fef4881883718fac883e839"} +ok +test_preparation_is_inert_atomic_and_reconciles_after_draft_edit (test_experiments.StudyTests.test_preparation_is_inert_atomic_and_reconciles_after_draft_edit) ... {"fixture_owner": "StudyTests", "new_schema": "catalog_96a28c51b1a44e57b0c2201fa17d3b39"} +ok + +---------------------------------------------------------------------- +Ran 75 tests in 246.263s + +OK +{"removed_exact_owned_schema": "catalog_1937048d1ac54ee8b748380c19a9fb16"} +{"removed_exact_owned_schema": "catalog_4c40d8d1c09a45fab9fea7f0def45479"} +{"removed_exact_owned_schema": "catalog_57796e8fbbc042a0b4f5a8144d5d3c53"} +{"removed_exact_owned_schema": "catalog_6bea37acaf5c4744aea607d2322ed54e"} +{"removed_exact_owned_schema": "catalog_8240f4833fef4881883718fac883e839"} +{"removed_exact_owned_schema": "catalog_96a28c51b1a44e57b0c2201fa17d3b39"} +{"removed_exact_owned_schema": "catalog_d491e85396da430cadc57c6faf285117"} +{"removed_exact_owned_schema": "catalog_f36b38b44a804167a349f2a8e4bbc1a6"} +{"started_utc": "2026-10-20T18:09:14.114818+00:00", "completed_utc": "2026-10-20T18:13:20.758064+00:00", "tests": 75, "failures": 0, "errors": 0, "skipped": 0, "owned_schemas_removed": ["catalog_1937048d1ac54ee8b748380c19a9fb16", "catalog_4c40d8d1c09a45fab9fea7f0def45479", "catalog_57796e8fbbc042a0b4f5a8144d5d3c53", "catalog_6bea37acaf5c4744aea607d2322ed54e", "catalog_8240f4833fef4881883718fac883e839", "catalog_96a28c51b1a44e57b0c2201fa17d3b39", "catalog_d491e85396da430cadc57c6faf285117", "catalog_f36b38b44a804167a349f2a8e4bbc1a6"], "no_constructor_or_domain_substitution": "Testfixture setUp wrappers only record exact fresh schema after unchanged setup; no production constructor or result patched.", "source_tree": "uncommitted isolated dependent branch from1442f184; exactsourcehashmanifest retained separately"} diff --git a/pipeline.py b/pipeline.py index b970300b..7dd0977b 100644 --- a/pipeline.py +++ b/pipeline.py @@ -91,7 +91,7 @@ def check(code, owner, phase, state, evidence, boundary, detail): return dict(report, sha256=digest(report)) -def _external_preflight_refusal(catalog_id, graph_id, revision, error, *, storage=False): +def _external_preflight_refusal(catalog_id, graph_id, revision, error, *, storage=False, material_ref=None): """Request provenance only: a refused preparation establishes no material proof.""" states = {'invalid': 'invalid', 'unsupported': 'unsupported', 'unavailable': 'blocked', 'stale': 'blocked', 'conflict': 'blocked', 'too_large': 'blocked', 'storage': 'blocked'} @@ -102,6 +102,9 @@ def _external_preflight_refusal(catalog_id, graph_id, revision, error, *, storag 'catalog_id': catalog_id, 'graph_id': graph_id, 'graph_revision': revision, 'execution_sha256': None, 'material_sha256': None, 'refusal': {'code': error.code, 'status': error.status}} + if material_ref is not None: + report.update(format='external_preflight_report_v3', recipe_material_ref=copy.deepcopy(material_ref), + model_ref=None, execution_material_sha256=None) evidence = digest(report) report['checks'] = [{'code': 'storage_capacity' if storage else 'material_preparation', 'owner': 'runs' if storage else 'pipeline', 'phase': 'preparation', 'state': state, @@ -125,6 +128,16 @@ def _external_preflight_refusal(catalog_id, graph_id, revision, error, *, storag return dict(report, sha256=digest(report)) +def _recipe_material_preflight_report(value, graph, prepared): + """Keep inner preparation proofs; bind their exact retained outer composition.""" + report = _external_preflight_report(graph, prepared, version=2) + report.update(format='external_preflight_report_v3', recipe_material_ref=copy.deepcopy(value['ref']), + model_ref=copy.deepcopy(value['material']['model']), material_sha256=value['ref']['sha256'], + execution_material_sha256=prepared['material_sha256']) + report['sha256'] = digest({key: item for key, item in report.items() if key != 'sha256'}) + return report + + def identifier(value, label='Identifier'): if not isinstance(value, str) or not ID.fullmatch(value): raise WorkbenchError(label + ' must be an opaque 32-character identifier.') @@ -1233,10 +1246,12 @@ def _retained_extension_static(self, composition, *, with_captured_inputs=False) raise WorkbenchError('Retained extension profile/context differs.','unavailable',409) return (static,recipe_contract.captured_inputs(execution,captured)) if with_captured_inputs else static - def _prepare_recipe_material(self, ref): + def _prepare_recipe_material(self, ref, *, _verified=None): """Revalidate source/runtime against retained bytes, never the current graph.""" from model_definitions import read_owned_blob,retained_json - value=self.recipe_material(ref,verify=True);material=value['material'] + value=self.recipe_material(ref,verify=True) if _verified is None else _verified + if value['ref'] != ref:raise WorkbenchError('Verified material reference differs.','unavailable',409) + material=value['material'] if material['trainer']['id'] in BUILTIN_TRAINERS:return self._prepare_builtin_recipe_material(value) raw=read_owned_blob(self.material_path,ref['sha256'],material['recipe_document'],2*1024**2) snapshot=retained_json(raw.decode(),2*1024**2) @@ -1269,7 +1284,23 @@ def _prepare_recipe_material(self, ref): return preflight,retained,graph @authorized('inspect', 'recipes.preflight_material') - def preflight_recipe_material(self, ref): + def preflight_recipe_material(self, ref, *, report=False): + if report: + from model_definitions import reference + ref=reference(ref,'recipe_material',self.db.dataset_id) + try: + value=self.recipe_material(ref,verify=True) + if value['material']['trainer']['id'] in BUILTIN_TRAINERS: + raise WorkbenchError('Read-only preflight does not support native builtin probes.','unsupported',409) + preflight,_retained,graph=self._prepare_recipe_material(ref,_verified=value) + # Reauthorize retained disclosure after potentially lengthy current-state checks. + current=self.recipe_material(ref,verify=True) + if current != value:raise WorkbenchError('Retained material changed during preflight.','stale',409) + return dict(value,preflight_report=_recipe_material_preflight_report(value,graph,preflight)) + except WorkbenchError as error: + projection=_external_preflight_refusal(self.db.dataset_id,None,None,error,material_ref=ref) + if projection is not None:error.preflight_report=projection + raise prepared=self._prepare_recipe_material(ref) if type(prepared) is dict:return prepared preflight,_retained,graph=prepared diff --git a/tests/test_material_preflight_reports.py b/tests/test_material_preflight_reports.py new file mode 100644 index 00000000..15727c77 --- /dev/null +++ b/tests/test_material_preflight_reports.py @@ -0,0 +1,252 @@ +"""Exact retained-material inspection and transport; no scientific acceptance.""" +import copy +from contextlib import redirect_stderr +import io +import json +import os +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch +from urllib.error import HTTPError +from urllib.request import Request, urlopen + +import agent_api as api +import pipeline +import project_cli +import test_run_preflight_report as graph_reports +import test_run_preflight_refusals as graph_refusals +from workbench import WorkbenchError + + +class MaterialPreflightReportTests(unittest.TestCase): + def setUp(self): + fixture=graph_reports.PreflightReportTests('runTest');fixture.setUp() + self.graph,self.prepared=fixture.graph,fixture.prepared + catalog=self.prepared['material']['catalog_id'] + def ref(kind,name,revision=True): + result={'kind':kind,'catalog_id':catalog,'id':name,'sha256':'c'*64} + if revision:result['revision']=1 + return result + blob={'name':'recipe.json','sha256':'d'*64,'bytes':10,'media_type':'application/json'} + material={'format':'recipe_material_v1','catalog_id':catalog,'recipe_id':self.graph['id'], + 'recipe_revision':self.graph['revision'],'recipe_document':blob, + 'executable_sha256':self.prepared['material_sha256'],'model':ref('model','e'*32), + 'data':[],'trainer':ref('trainer','external_recipe_v1'), + 'evaluation_profile':ref('profile','opaque_external_outputs_v1'), + 'resolved_profile':dict(blob,name='profile.json'), + 'runtime':ref('runtime',pipeline.digest(self.prepared['material']['runtime']),False), + 'captures':[dict(blob,name='capture-0000.bin')], + 'resource_envelope':{key:1 for key in ('attempt_slots','admitted_wall_seconds','retained_bytes','peak_staging_bytes')}} + material['runtime']['sha256']=material['runtime']['id'] + sha=pipeline.digest(material) + self.value={'ref':{'kind':'recipe_material','catalog_id':catalog,'id':sha,'sha256':sha},'material':material} + + def success(self): + return {'status':200,'result':dict(copy.deepcopy(self.value),preflight_report= + pipeline._recipe_material_preflight_report(self.value,self.graph,self.prepared))} + + def refusal(self,code='stale',status=409): + error=WorkbenchError('Private detail',code,status) + report=pipeline._external_preflight_refusal(self.value['ref']['catalog_id'],None,None,error, + material_ref=self.value['ref']) + return {'status':status,'result':{'error':str(error),'code':code,'preflight_report':report}} + + def test_distinct_outer_inner_and_graph_identities_and_deferred_execution(self): + value=self.success();api.validate_output('recipes.preflight_material',value) + report=value['result']['preflight_report'] + self.assertEqual(report['material_sha256'],self.value['ref']['sha256']) + self.assertEqual(report['execution_material_sha256'],self.prepared['material_sha256']) + self.assertEqual(report['execution_sha256'],self.prepared['execution_sha256']) + self.assertEqual(report['model_ref'],self.value['material']['model']) + self.assertEqual(report['admission'],'not_reserved') + original=pipeline._external_preflight_report(self.graph,self.prepared,version=2) + self.assertEqual(report['checks'],original['checks']) + self.assertEqual(len(report['checks']),10) + self.assertLess(len(json.dumps(report).encode()),8192) + self.assertFalse(api.operation('recipes.preflight_material')['side_effects']) + self.assertEqual(api.operation('recipes.preflight_material')['scopes'],['inspect']) + + def test_refusal_is_bounded_request_provenance_with_unknown_identities(self): + for code in ('invalid','unsupported','unavailable','stale','conflict','too_large','storage'): + value=self.refusal(code);api.validate_output('recipes.preflight_material',value) + report=value['result']['preflight_report'] + self.assertEqual(report['recipe_material_ref'],self.value['ref']) + for key in ('graph_id','graph_revision','model_ref','execution_material_sha256', + 'execution_sha256','material_sha256'):self.assertIsNone(report[key]) + self.assertFalse(any(row['state']=='checked' for row in report['checks'])) + self.assertNotIn('Private detail',json.dumps(report)) + for status in (401,403): + api.validate_output('recipes.preflight_material',{'status':status,'result':{'error':'Denied','code':'forbidden'}}) + + def test_consumer_rejects_forged_links_proofs_owners_states_and_bounds(self): + mutations=(lambda r:r.update(material_sha256='a'*64), + lambda r:r.update(execution_material_sha256='a'*64),lambda r:r.update(execution_sha256=None), + lambda r:r.update(graph_revision=3),lambda r:r.update(model_ref=None), + lambda r:r['recipe_material_ref'].update(id='a'*64),lambda r:r['checks'][0].update(owner='runs'), + lambda r:r['checks'][-1].update(state='checked'),lambda r:r['checks'].pop(), + lambda r:r['checks'][0].update(detail='x'*241)) + for mutate in mutations: + value=self.success();report=value['result']['preflight_report'];mutate(report) + graph_refusals.PreflightRefusalTests.rehash(report) + with self.assertRaises(api.AgentAPIError):api.validate_output('recipes.preflight_material',value) + for key,value in (('graph_id','a'*32),('graph_revision',1),('model_ref',self.value['material']['model']), + ('execution_material_sha256','a'*64)): + envelope=self.refusal();report=envelope['result']['preflight_report'];report[key]=value + graph_refusals.PreflightRefusalTests.rehash(report) + with self.assertRaises(api.AgentAPIError):api.validate_output('recipes.preflight_material',envelope) + + def test_consumer_refuses_rehashed_foreign_or_builtin_compositions_and_runtime_provenance(self): + for key in ('model','trainer','evaluation_profile','runtime'): + value=self.success();result=value['result'];material=result['material'];report=result['preflight_report'] + material[key]['catalog_id']='a'*32 + sha=pipeline.digest(material);result['ref'].update(id=sha,sha256=sha) + report.update(recipe_material_ref=copy.deepcopy(result['ref']),material_sha256=sha, + model_ref=copy.deepcopy(material['model'])) + graph_refusals.PreflightRefusalTests.rehash(report) + with self.assertRaises(api.AgentAPIError):api.validate_output('recipes.preflight_material',value) + for change in (lambda m:m['trainer'].update(id=pipeline.BUILTIN_TRAINERS[0]), + lambda m:m['runtime'].update(id='a'*64)): + value=self.success();result=value['result'];report=result['preflight_report'];change(result['material']) + sha=pipeline.digest(result['material']);result['ref'].update(id=sha,sha256=sha) + report.update(recipe_material_ref=copy.deepcopy(result['ref']),material_sha256=sha) + graph_refusals.PreflightRefusalTests.rehash(report) + with self.assertRaises(api.AgentAPIError):api.validate_output('recipes.preflight_material',value) + for code in ('runtime_inventory','runtime_ready_go','authority_admission','custody_completion','output_publication'): + value=self.success();report=value['result']['preflight_report'] + next(row for row in report['checks'] if row['code']==code)['evidence_sha256']='a'*64 + graph_refusals.PreflightRefusalTests.rehash(report) + with self.assertRaises(api.AgentAPIError):api.validate_output('recipes.preflight_material',value) + + def test_builtin_refuses_before_probe_and_early_foreign_ref_stays_bare(self): + value=copy.deepcopy(self.value);value['material']['trainer']['id']=pipeline.BUILTIN_TRAINERS[0] + owner=SimpleNamespace(access=Mock(),db=SimpleNamespace(dataset_id=self.value['ref']['catalog_id']), + recipe_material=Mock(return_value=value),_prepare_recipe_material=Mock()) + with patch('subprocess.Popen',side_effect=AssertionError('No native probe')): + with self.assertRaises(WorkbenchError) as caught: + pipeline.Pipelines.preflight_recipe_material(owner,self.value['ref'],report=True) + self.assertEqual(caught.exception.code,'unsupported');owner._prepare_recipe_material.assert_not_called() + api.validate_output('recipes.preflight_material',{'status':caught.exception.status, + 'result':{'error':str(caught.exception),'code':caught.exception.code, + 'preflight_report':caught.exception.preflight_report}}) + owner.recipe_material.reset_mock() + with self.assertRaises(WorkbenchError) as caught: + pipeline.Pipelines.preflight_recipe_material(owner,dict(self.value['ref'],catalog_id='a'*32),report=True) + self.assertIsNone(getattr(caught.exception,'preflight_report',None));owner.recipe_material.assert_not_called() + + def test_default_internal_path_and_private_verified_ref_fence_are_preserved(self): + owner=SimpleNamespace(access=Mock(),_prepare_recipe_material=Mock(return_value={'kind':'builtin'})) + self.assertEqual(pipeline.Pipelines.preflight_recipe_material(owner,self.value['ref']),{'kind':'builtin'}) + owner._prepare_recipe_material.assert_called_once_with(self.value['ref']) + wrong=copy.deepcopy(self.value);wrong['ref']['sha256']='a'*64 + with self.assertRaises(WorkbenchError): + pipeline.Pipelines._prepare_recipe_material(owner,self.value['ref'],_verified=wrong) + + def test_client_correlates_ref_catalog_and_real_error_transport_with_no_retry(self): + client=api.AgentClient('http://127.0.0.1:12345','k'*32,self.value['ref']['catalog_id']) + client._ready=True;client._selected={'recipes.preflight_material':1} + for value in (self.success(),self.refusal()): + client._request=Mock(return_value=value) + self.assertEqual(client.call('recipes.preflight_material',{'ref':self.value['ref']}),value) + client._request.reset_mock() + with self.assertRaises(api.AgentAPIError): + client.call('recipes.preflight_material',{'ref':dict(self.value['ref'],sha256='a'*64)}) + self.assertEqual(client._request.call_count,1) + value=self.refusal();value['result'].update(error='k'*32,unrelated='discard') + fixture=graph_refusals.PreflightRefusalTests('runTest') + with fixture.transport(value) as base: + client=api.AgentClient(base,'k'*32,self.value['ref']['catalog_id']) + client._ready=True;client._selected={'recipes.preflight_material':1} + result=client.call('recipes.preflight_material',{'ref':self.value['ref']}) + self.assertEqual(result['result']['error'],'[redacted]');self.assertNotIn('unrelated',result['result']) + stderr=io.StringIO() + with tempfile.NamedTemporaryFile(mode='w+',suffix='.json') as arguments: + json.dump({'ref':self.value['ref']},arguments);arguments.flush() + with patch.object(api.AgentClient,'from_config',return_value=client),redirect_stderr(stderr): + code=project_cli.main(['--config','/unused','call','--operation','recipes.preflight_material', + '--arguments',arguments.name]) + self.assertEqual(code,1);self.assertEqual(json.loads(stderr.getvalue())['preflight_report'], + value['result']['preflight_report']) + value['result']['preflight_report']=None + with self.assertRaises(api.AgentAPIError):client.call('recipes.preflight_material',{'ref':self.value['ref']}) + + +@unittest.skipUnless(os.environ.get('TULDOK_DATABASE_URL'),'Requires disposable PostgreSQL') +class MaterialPreflightHTTPTests(unittest.TestCase): + def fixture(self): + import test_experiments as studies + case=studies.StudyTests('runTest');self.addCleanup(case.doCleanups);case.setUp() + self.schema=case.dataset.db.schema + return case + + def test_actual_study_material_http_cli_drift_cold_read_and_replay_are_inert(self): + case=self.fixture();trial,body=case.prepare();ref=trial['material'] + base=case.server();client=api.AgentClient(base,case.agent['token'],case.dataset.db.dataset_id) + with case.owner.bound(case.dataset): + before=case.studies.get(case.study['id']) + tables=('workspace_recipe_materials','model_trials','workspace_runs','access_execution_grants', + 'access_execution_consumptions','model_attempt_reservations') + counts={name:case.dataset.db.execute('SELECT count(*) FROM '+name).fetchone()[0] for name in tables} + stored=case.dataset.pipelines.recipe_material(ref,verify=True) + graph=copy.deepcopy(case.graph);graph['name']='New draft after retained preparation' + case.dataset.pipelines.save(graph['id'],{'revision':graph['revision'],'document':graph}) + with patch('subprocess.Popen',side_effect=AssertionError('Preflight cannot execute')): + value=client.call('recipes.preflight_material',{'ref':ref}) + self.assertEqual(value['status'],200) + report=value['result']['preflight_report'] + self.assertEqual(report['graph_revision'],case.graph['revision']) + self.assertEqual({k:v for k,v in value['result'].items() if k!='preflight_report'},stored) + (case.project_path/'recipe.py').write_bytes(case.source+b'\n# actual source drift\n') + refused=client.call('recipes.preflight_material',{'ref':ref}) + self.assertEqual(refused['status'],409) + self.assertIsNone(refused['result']['preflight_report']['model_ref']) + self.assertIsNone(refused['result']['preflight_report']['execution_sha256']) + stderr=io.StringIO() + with tempfile.NamedTemporaryFile(mode='w+',suffix='.json') as arguments: + json.dump({'ref':ref},arguments);arguments.flush() + with patch.object(api.AgentClient,'from_config',return_value=client),redirect_stderr(stderr): + code=project_cli.main(['--config','/unused','call','--operation','recipes.preflight_material', + '--arguments',arguments.name]) + self.assertEqual(code,1) + self.assertEqual(json.loads(stderr.getvalue())['preflight_report'],refused['result']['preflight_report']) + self.assertEqual(client.call('recipes.material',{'ref':ref})['result'],stored) + with case.owner.bound(case.dataset): + self.assertEqual(case.studies.prepare_trial(case.study['id'],body),trial) + self.assertEqual(case.studies.get(case.study['id']),before) + self.assertEqual(case.dataset.runs.list(),[]) + self.assertEqual(case.dataset.db.execute('SELECT count(*) FROM access_execution_consumptions').fetchone()[0],0) + self.assertEqual({name:case.dataset.db.execute('SELECT count(*) FROM '+name).fetchone()[0] + for name in tables},counts) + request=Request(base+'/api/workspace/recipe-material/preflight',data=api.encoded({'ref':ref}), + headers={'Authorization':'Bearer '+case.agent['token'],'Content-Type':'application/json'}) + with patch.object(case.dataset.pipelines,'preflight_recipe_material') as prepare: + with self.assertRaises(HTTPError) as caught:urlopen(request,timeout=20) + self.assertEqual(caught.exception.code,409) + self.assertNotIn('preflight_report',json.loads(caught.exception.read()));prepare.assert_not_called() + bad=api.AgentClient(base,'x'*32,case.dataset.db.dataset_id) + bad._ready=True;bad._selected={'recipes.preflight_material':1} + with patch.object(case.dataset.pipelines,'preflight_recipe_material') as prepare: + denied=bad.call('recipes.preflight_material',{'ref':ref}) + self.assertEqual(denied['status'],401);self.assertNotIn('preflight_report',denied['result']) + prepare.assert_not_called() + + def test_current_disclosure_rechecked_and_retained_tamper_has_unknown_proofs(self): + case=self.fixture();trial,_=case.prepare();ref=trial['material'] + with case.owner.bound(case.dataset): + value=case.dataset.pipelines.recipe_material(ref,verify=True) + from access import AccessError + with patch.object(case.dataset.pipelines,'recipe_material',side_effect=[value,AccessError('Revoked')]): + with self.assertRaises(AccessError) as caught: + case.dataset.pipelines.preflight_recipe_material(ref,report=True) + self.assertIsNone(getattr(caught.exception,'preflight_report',None)) + item=value['material']['captures'][0] + path=case.dataset.pipelines.material_path/ref['sha256']/item['name'] + path.chmod(0o600);path.write_bytes(b'x'*item['bytes']) + with self.assertRaises(WorkbenchError) as caught: + case.dataset.pipelines.preflight_recipe_material(ref,report=True) + report=caught.exception.preflight_report + self.assertEqual(report['state'],'blocked');self.assertIsNone(report['material_sha256']) + self.assertFalse(any(row['state']=='checked' for row in report['checks'])) + + +if __name__=='__main__':unittest.main()