Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ Preview it locally from the repository root with `python -m http.server 8000`, t

## Professional portal demonstration

A separate local-only professional workflow portal is available for synthetic demonstrations. It
supports project creation, canonical Phase 5 feature preparation on mathematical terrain, screening
hypotheses, human review, evidence and audit records, and limitations-first reports. It does **not**
load the private model, accept real terrain, or provide live commercial inference.
A separate local-only professional workflow portal supports project creation, canonical Phase 5
feature preparation on mathematical terrain, screening outputs, human review, evidence and audit
records, and limitations-first reports. The default uses a deterministic demonstration scorer.
Phase 6D also provides an explicit localhost-only mode that executes the hash-verified frozen E001
Random Forest on the same synthetic terrain. Neither mode accepts real terrain or coordinates.

```powershell
python -m pip install -e ".[portal]"
Expand All @@ -58,7 +59,9 @@ archaeoai portal --demo --reset

Open `http://127.0.0.1:8000`. Read the
[Phase 6C implementation note](docs/product/PHASE_6C_COMMERCIAL_MVP.md) and
[portal runbook](docs/product/PORTAL_RUNBOOK.md) for boundaries and operator steps.
[portal runbook](docs/product/PORTAL_RUNBOOK.md) for boundaries and operator steps. The narrowly
approved real-model path is documented in the
[Phase 6D runtime note](docs/product/PHASE_6D_APPROVED_MODEL_RUNTIME.md).

## What is ArchaeoAI?

Expand Down
73 changes: 73 additions & 0 deletions docs/product/PHASE_6D_APPROVED_MODEL_RUNTIME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Phase 6D approved private model runtime

Phase 6D connects the frozen E001 Random Forest to the local portal while retaining a deliberately
narrow input boundary: the real model can score only deterministic mathematical terrain generated
inside the application. Real terrain, uploads, coordinates, remote inference, training, tuning, and
archaeological determination remain out of scope.

## Approved identity

- Model: `E001_FROZEN_RANDOM_FOREST` (`sklearn.ensemble.RandomForestClassifier`)
- Configuration SHA-256: `20cd377c17373eeeb5403c84119084287f193d93b42c8004d99c823e01a157e4`
- Private artifact SHA-256: `50f7968069ecaa1e0016f37be6356531ab3f26802c806efb5dc8fb2e295a503f`
- Learned-state SHA-256: `e3b0c072f437e889f09a2a2cf5a37f19b2f483eb5188e102b132a89ee76d1939`
- Private ignored location: `data/private/e001/inference/e001_phase2f_random_forest.pkl`

The path is fixed in the inference system. It is not accepted from the CLI, browser, or API.

## Runtime architecture

The owner explicitly authorizes the runtime at server startup. The loader resolves the repository
root and exact private location, confirms private containment and Git-ignore protection, verifies the
artifact digest before pickle deserialization, then validates the exact estimator class, learned
state, and frozen configuration identity. Any mismatch stops startup. It never searches for another
model, rebuilds one, or falls back to the demonstration scorer.

For each approved run, the portal creates a deterministic 128 × 128 mathematical elevation surface,
uses the canonical four-representation Phase 5 transform and immutable 4,096-value `float32` model
input, and invokes the frozen Random Forest probability-scoring path. Features are discarded rather
than stored. Results record `AI_OUTPUT`, `APPROVED_PRIVATE_RANDOM_FOREST`, and
`PERFORMED_APPROVED_PRIVATE_MODEL` before entering the human-review workflow.

Pickle can execute code during deserialization. This implementation therefore permits only the
single private, hash-bound artifact. Possession of repository or browser access does not authorize a
different pickle.

## Safe interpretation

The output is a **bounded terrain-pattern similarity score from the frozen E001 model**. It is not an
archaeological probability, archaeological confidence, discovery probability, or determination of
presence or absence. Synthetic input and required human review remain visible in the UI, API-safe
provenance, audit trail, and report.

## Launch

Default synthetic demonstration, including Web Preview:

```powershell
archaeoai portal --demo --reset --host 0.0.0.0
```

Real frozen model on synthetic terrain, local/private only:

```powershell
archaeoai portal --demo --approved-model-runtime --reset
```

Open the latter at `http://127.0.0.1:8000`. Approved mode rejects `--host 0.0.0.0`; the default remains
`127.0.0.1`.

## Expected closed failures

Startup stops with a fixed safe message if the private artifact is missing, outside the approved
private boundary, not ignored, changed, not the exact classifier type, or different in learned state.
An API request cannot authorize a runtime that the server did not authorize at startup. Runtime and
request errors do not reveal filesystem paths, pickle details, features, or model internals.

## Still unauthorized

This phase does not complete Phase 5E independent review, authorize Phase 5F, establish pilot
readiness, or validate a commercial product. Real/customer terrain, file upload, public or remote
model execution, candidate-location publication, and archaeological discovery claims remain
prohibited. A later owner-approved gate must resolve external review findings before any broader
interface or real-data work.
34 changes: 26 additions & 8 deletions docs/product/PORTAL_RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ArchaeoAI portal runbook

This runbook operates the Phase 6C local synthetic demonstration. It does not enable live inference.
This runbook operates the local portal in either its default synthetic-scorer mode or its explicit
Phase 6D approved-model mode. Both modes accept only generated mathematical terrain.

## 1. Install

Expand Down Expand Up @@ -45,23 +46,39 @@ Open:

`http://127.0.0.1:8000`

Enter the demo workspace, then create, authorize, and run a synthetic project. The approved model
status must remain `DISABLED / NOT AUTHORIZED`, and each synthetic result must report
Enter the demo workspace, then create, authorize, and run a synthetic project. In default mode the
approved model status remains `DISABLED / NOT AUTHORIZED`, and each result reports
`MODEL_EXECUTION: NOT_PERFORMED`.

## 5. Exercise the workflow
## 5. Optional approved private runtime

Confirm the exact frozen artifact already exists at its approved ignored location, then launch:

```powershell
archaeoai portal --demo --approved-model-runtime --reset
```

This mode binds only to `127.0.0.1`. It verifies the hash-bound private model before deserialization,
then permits the frozen model to score synthetic terrain. The UI should show
`REAL FROZEN MODEL · SYNTHETIC TERRAIN · LOCAL PRIVATE`; approved results report `AI_OUTPUT` and
`PERFORMED_APPROVED_PRIVATE_MODEL`. Never interpret the score as archaeological probability.

Do not combine `--approved-model-runtime` with `--host 0.0.0.0`. Missing or invalid artifacts stop
startup without retraining, downloading, searching for alternatives, or using a fallback score.

## 6. Exercise the workflow

Use Projects to create or open a project, Results to filter synthetic hypotheses, Review to record a
human observation, Report to generate a print-ready summary, Audit to inspect events, and Settings to
change retention or delete the local project.

No screen should request real terrain, coordinates, model files, or archaeological identifiers.

## 6. Stop the service
## 7. Stop the service

Return to the terminal and press `Ctrl+C`.

## 7. Locate or delete local demo state
## 8. Locate or delete local demo state

The database is:

Expand All @@ -75,5 +92,6 @@ that SQLite file to remove all retained demo state. Running again with `--reset`
- If the portal extra is missing, run `python -m pip install -e ".[portal]"`.
- If the port is occupied, use `archaeoai portal --demo --port 8001` and open the reported address.
- Ports below 1024 and non-local bind addresses are not supported.
- If approved-model mode is requested, the expected Phase 6C result is the controlled error
`APPROVED_MODEL_RUNTIME_NOT_AUTHORIZED`.
- If approved-model startup fails, verify only the owner-managed approved private artifact. Browser
requests cannot enable the model, and the portal intentionally exposes no model-path setting.
- Approved-model mode is localhost-only. Use the synthetic scorer for Web Preview.
6 changes: 6 additions & 0 deletions src/archaeoai/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,11 @@ def build_parser() -> SafeArgumentParser:
help="launch the local synthetic professional portal demonstration",
)
portal.add_argument("--demo", action="store_true", help="use synthetic demonstration mode")
portal.add_argument(
"--approved-model-runtime",
action="store_true",
help="owner-authorize the frozen private model for synthetic terrain on localhost only",
)
portal.add_argument("--reset", action="store_true", help="reset and seed local demo data")
portal.add_argument("--port", type=int, default=8000, help="localhost port (default: 8000)")
portal.add_argument(
Expand All @@ -416,6 +421,7 @@ def main(argv: list[str] | None = None) -> int:
reset=args.reset,
port=args.port,
host=args.host,
approved_model_runtime=args.approved_model_runtime,
)
except Exception:
print("ERROR: the local portal failed safely.", file=sys.stderr)
Expand Down
12 changes: 12 additions & 0 deletions src/archaeoai/inference_system/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@
GeoTIFFValidationError,
load_canonical_geotiff,
)
from archaeoai.inference_system.private_model_adapter import (
FROZEN_MODEL_STATE_SHA256,
ApprovedPrivateModelLoadError,
ApprovedPrivateRandomForestAdapter,
approved_private_model_reference,
load_approved_private_random_forest,
)
from archaeoai.inference_system.single_patch import (
APPROVED_MODEL_ARTIFACT_SHA256,
APPROVED_MODEL_RELATIVE_PATH,
Expand All @@ -62,6 +69,8 @@
"APPROVED_MODEL_ARTIFACT_SHA256",
"APPROVED_MODEL_RELATIVE_PATH",
"ApprovedModelArtifactReference",
"ApprovedPrivateModelLoadError",
"ApprovedPrivateRandomForestAdapter",
"AutomaticInferenceResult",
"BATCH_MANIFEST_SCHEMA_VERSION",
"BATCH_RESULT_SCHEMA_VERSION",
Expand All @@ -74,6 +83,7 @@
"BatchRunResult",
"CanonicalGeoTIFF",
"EvidenceLevel",
"FROZEN_MODEL_STATE_SHA256",
"GeoTIFFErrorCode",
"GeoTIFFValidationError",
"LimitationCode",
Expand All @@ -96,10 +106,12 @@
"WarningCode",
"verify_model_artifact_checksum",
"load_batch_manifest",
"load_approved_private_random_forest",
"load_canonical_geotiff",
"run_feature_batch",
"run_approved_single_patch_inference",
"score_single_patch_model_adapter",
"transform_single_patch",
"verify_approved_model_artifact",
"approved_private_model_reference",
]
113 changes: 113 additions & 0 deletions src/archaeoai/inference_system/private_model_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""Hash-bound loader and adapter for the one approved private E001 model."""

from __future__ import annotations

import math
from dataclasses import dataclass, field
from pathlib import Path

import numpy as np
from sklearn.ensemble import RandomForestClassifier

from archaeoai.inference import FEATURE_COUNT, load_private_model, model_state_sha256
from archaeoai.inference import score_feature_matrix as score_frozen_feature_matrix
from archaeoai.inference_system.contracts import (
APPROVED_MODEL_CONFIG_SHA256,
ModelIdentifier,
)
from archaeoai.inference_system.single_patch import (
APPROVED_MODEL_ARTIFACT_SHA256,
APPROVED_MODEL_RELATIVE_PATH,
ApprovedModelArtifactReference,
verify_approved_model_artifact,
)
from archaeoai.terrain.privacy import ensure_private_output, verify_git_ignored

FROZEN_MODEL_STATE_SHA256 = "e3b0c072f437e889f09a2a2cf5a37f19b2f483eb5188e102b132a89ee76d1939"


class ApprovedPrivateModelLoadError(RuntimeError):
"""Safe public failure for an unavailable or invalid approved model runtime."""

code = "APPROVED_PRIVATE_MODEL_UNAVAILABLE"


@dataclass(frozen=True, slots=True)
class ApprovedPrivateRandomForestAdapter:
"""Expose only frozen single-patch scoring from a verified private estimator."""

_estimator: RandomForestClassifier = field(repr=False)
model_identifier: ModelIdentifier = ModelIdentifier.E001_FROZEN_RANDOM_FOREST
model_config_sha256: str = APPROVED_MODEL_CONFIG_SHA256[
ModelIdentifier.E001_FROZEN_RANDOM_FOREST
]
model_artifact_sha256: str = APPROVED_MODEL_ARTIFACT_SHA256

def __post_init__(self) -> None:
if self.model_identifier is not ModelIdentifier.E001_FROZEN_RANDOM_FOREST:
raise ValueError("approved private model identifier mismatch")
if self.model_config_sha256 != APPROVED_MODEL_CONFIG_SHA256[self.model_identifier]:
raise ValueError("approved private model configuration identity mismatch")
if self.model_artifact_sha256 != APPROVED_MODEL_ARTIFACT_SHA256:
raise ValueError("approved private model artifact identity mismatch")
if type(self._estimator) is not RandomForestClassifier:
raise TypeError("approved adapter requires the exact RandomForestClassifier class")
if model_state_sha256(self._estimator) != FROZEN_MODEL_STATE_SHA256:
raise ValueError("approved private model state mismatch")

def score_model_input(self, feature_matrix: np.ndarray) -> float:
"""Score one immutable frozen feature row through the canonical implementation."""
if (
not isinstance(feature_matrix, np.ndarray)
or feature_matrix.shape != (1, FEATURE_COUNT)
or feature_matrix.dtype != np.float32
or feature_matrix.flags.writeable
or not feature_matrix.flags.c_contiguous
or not np.isfinite(feature_matrix).all()
):
raise ValueError("approved model input contract mismatch")
scores = score_frozen_feature_matrix(self._estimator, feature_matrix)
if scores.shape != (1,) or not math.isfinite(float(scores[0])) or not 0 <= scores[0] <= 1:
raise ValueError("approved model output contract mismatch")
return float(scores[0])


def approved_private_model_reference(project_root: Path) -> ApprovedModelArtifactReference:
"""Build the only authorized reference without accepting a caller-supplied path."""
root = Path(project_root).resolve()
return ApprovedModelArtifactReference(
path=root / APPROVED_MODEL_RELATIVE_PATH,
model_identifier=ModelIdentifier.E001_FROZEN_RANDOM_FOREST,
model_config_sha256=APPROVED_MODEL_CONFIG_SHA256[ModelIdentifier.E001_FROZEN_RANDOM_FOREST],
)


def load_approved_private_random_forest(
project_root: Path,
) -> ApprovedPrivateRandomForestAdapter:
"""Verify every approved identity before allowing private pickle deserialization."""
root = Path(project_root).resolve()
reference = approved_private_model_reference(root)
try:
private_path = ensure_private_output(root, reference.path)
if private_path != (root / APPROVED_MODEL_RELATIVE_PATH).resolve():
raise ValueError("unexpected approved model path")
verify_git_ignored(root, private_path)
verified_artifact_sha256 = verify_approved_model_artifact(root, reference)
if verified_artifact_sha256 != APPROVED_MODEL_ARTIFACT_SHA256:
raise ValueError("approved model artifact identity mismatch")
estimator = load_private_model(
root,
private_path,
expected_artifact_sha256=APPROVED_MODEL_ARTIFACT_SHA256,
expected_state_sha256=FROZEN_MODEL_STATE_SHA256,
)
if type(estimator) is not RandomForestClassifier:
raise ValueError("approved model class mismatch")
if model_state_sha256(estimator) != FROZEN_MODEL_STATE_SHA256:
raise ValueError("approved model state mismatch")
return ApprovedPrivateRandomForestAdapter(estimator)
except ApprovedPrivateModelLoadError:
raise
except Exception as exc:
raise ApprovedPrivateModelLoadError(ApprovedPrivateModelLoadError.code) from exc
Loading
Loading