Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c1e0cd5
feat: add deterministic provenance archive envelope
seonghobae Aug 31, 2026
55da6e2
fix: harden provenance archive validation
seonghobae Aug 31, 2026
dfc2caf
fix: enforce provenance archive profile bounds
seonghobae Aug 31, 2026
80f2fe3
fix: reject provenance archive interior gaps
seonghobae Aug 31, 2026
3fd36a5
feat: round trip tenant provenance closure
seonghobae Aug 31, 2026
236423f
fix: preserve provenance citation closure
seonghobae Aug 31, 2026
67be568
fix: harden provenance bundle validation
seonghobae Aug 31, 2026
832798e
fix: classify compound provenance secrets
seonghobae Aug 31, 2026
9d78942
fix: tokenize provenance metadata keys
seonghobae Aug 31, 2026
aca3f3a
feat(data): add signed provenance bundle API
seonghobae Aug 31, 2026
b848a39
fix(data): require authoritative provenance scope
seonghobae Aug 31, 2026
e1c9978
test: align bootstrap smoke with current schema
seonghobae Aug 31, 2026
7a19950
docs: record bounded provenance portability slice
seonghobae Aug 31, 2026
1809fc6
fix: skip legacy email index on fresh bootstrap
seonghobae Aug 31, 2026
eaade6e
docs: record fresh bootstrap verification
seonghobae Aug 31, 2026
63ec9f3
fix(provenance): scope portable identity mappings
seonghobae Aug 31, 2026
7576977
fix(provenance): harden scoped identity imports
seonghobae Aug 31, 2026
4fbac3d
fix(provenance): preserve untyped metadata strings
seonghobae Aug 31, 2026
73f1740
fix(provenance): require verified archive membership
seonghobae Aug 31, 2026
092e393
fix(provenance): export mixed identity origins
seonghobae Aug 31, 2026
9da29fd
fix(provenance): reject foreign segment citations
seonghobae Aug 31, 2026
4bd6589
fix(provenance): compose import transactions
seonghobae Aug 31, 2026
26c8513
fix(provenance): bind citations to selected records
seonghobae Aug 31, 2026
176540c
fix(provenance): allow anchored segment edges
seonghobae Aug 31, 2026
c53703a
fix(provenance): serialize shared email imports
seonghobae Aug 31, 2026
4b3ae1a
fix: validate imported provenance citation anchors
seonghobae Aug 31, 2026
03de223
fix(provenance): lock overlapping email imports
seonghobae Aug 31, 2026
d855dc7
fix: require canonical provenance timestamps
seonghobae Aug 31, 2026
42795bd
fix(provenance): reuse complete identity mappings
seonghobae Aug 31, 2026
fe0ad99
fix: canonicalize provenance activity timestamps
seonghobae Aug 31, 2026
66443a9
fix(provenance): remap typed edge endpoints
seonghobae Aug 31, 2026
473ab32
fix(provenance): validate nullable edge endpoints
seonghobae Aug 31, 2026
8ae8bd5
fix: bind provenance bundle identities to content
seonghobae Aug 31, 2026
c7ca74d
fix(provenance): bound export row loading
seonghobae Aug 31, 2026
053b406
fix(provenance): preserve portable import origins
seonghobae Aug 31, 2026
69ab128
fix(provenance): serialize portable identity imports
seonghobae Aug 31, 2026
0034213
fix(data): preserve incremental provenance imports
seonghobae Sep 1, 2026
e2968cd
test(db): fail on post-connect backfill errors
seonghobae Sep 1, 2026
152d199
test(db): dispose pool after close failures
seonghobae Sep 1, 2026
705d8ec
Merge branch 'codex/pdf-dom-upload-64m' of https://github.com/Context…
seonghobae Sep 5, 2026
7ee6e68
fix(provenance): integrate owner migrations and retain restore identi…
seonghobae Sep 5, 2026
69f50ae
fix(provenance): integrate full-document storage without losing ident…
seonghobae Sep 5, 2026
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
## [Unreleased]
- Proposed: export and restore the project evidence cited in one verified
workspace, preserving source links and stable identities. This first slice
excludes full mailbox, binary documents, credentials, provider and connector
state, embeddings, and audit history. Archives remain bounded to 64 MiB
compressed and total uncompressed content, 64 entries, 32 MiB per entry, and
a 100:1 compression ratio; invalid archives are rejected before changes.
Full customer-exit portability and deployment verification remain open.
- Proposed: accept manual PDF uploads up to 64MiB after the required processing
service release is verified and pinned. Larger files are rejected before
storage or processing with HTTP 413; split the file and upload it again.
Expand Down
59 changes: 59 additions & 0 deletions backend/alembic/versions/0018_provenance_identity_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Add scoped portable-to-database provenance identity mappings."""

from alembic import op
import sqlalchemy as sa


revision = "0018_provenance_identity"
down_revision = "0017_merge_newsdom_carddav_heads"
branch_labels = None
depends_on = None
_MAPPING_TABLE = "provenance_identity_mappings"


def upgrade() -> None:
inspector = sa.inspect(op.get_bind())
if not inspector.has_table(_MAPPING_TABLE):
op.create_table(
_MAPPING_TABLE,
sa.Column("provenance_identity_id", sa.Integer(), primary_key=True),
sa.Column("target_user_id", sa.String(), nullable=False),
sa.Column("target_organization_id", sa.String(), nullable=False),
sa.Column("target_workspace_id", sa.String(), nullable=False),
sa.Column("source_user_uid", sa.String(length=64), nullable=False),
sa.Column("source_organization_uid", sa.String(), nullable=False),
sa.Column("source_workspace_uid", sa.String(), nullable=False),
sa.Column("entity_kind", sa.String(length=64), nullable=False),
sa.Column("portable_uid", sa.String(length=256), nullable=False),
sa.Column("target_database_uid", sa.String(length=96), nullable=False),
sa.UniqueConstraint(
"target_user_id",
"target_organization_id",
"target_workspace_id",
"source_user_uid",
"source_organization_uid",
"source_workspace_uid",
"entity_kind",
"portable_uid",
name="uq_provenance_identity_source_target",
),
sa.UniqueConstraint(
"entity_kind",
"target_database_uid",
name="uq_provenance_identity_target_uid",
),
)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
op.create_index(
"ix_provenance_identity_target_scope",
_MAPPING_TABLE,
["target_user_id", "target_organization_id", "target_workspace_id"],
if_not_exists=True,
)


def downgrade() -> None:
"""Preserve imported identity mappings when application code is rolled back."""
# The table may predate this revision via 0001's live metadata bootstrap.
# Its portable identity history is not rebuildable from remapped records.
# Destructive retirement requires a separate, explicitly governed operation.
return None
16 changes: 16 additions & 0 deletions backend/alembic/versions/0021_merge_provenance_workspace.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Join provenance identity and workspace repair histories without rewriting either."""

revision = "0021_merge_provenance_workspace"
down_revision = ("0018_provenance_identity", "0020_search_trigram_storage")
branch_labels = None
depends_on = None


def upgrade() -> None:
"""Require both existing branches before recording the unified head."""
return None


def downgrade() -> None:
"""Reopen the two revision heads without changing customer records."""
return None
90 changes: 89 additions & 1 deletion backend/api/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import re
from typing import Literal, NamedTuple

from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, Response, UploadFile
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy import and_, case, func, or_, select
from sqlalchemy.engine import Row
Expand Down Expand Up @@ -35,6 +35,13 @@
PDF_DOM_RECOGNITION_PENDING_STATUS,
)
from services.ontology_service import ontology_service
from services.tenant_provenance_bundle import (
ARCHIVE_MAX_BYTES,
ProvenanceArchiveError,
TenantProvenanceScope,
export_tenant_provenance,
import_tenant_provenance,
)
from services.webdav_service import webdav_service
from services.workspace_scope import get_or_create_workspace

Expand All @@ -46,6 +53,7 @@
# transport ceiling so large customer PDFs are not accepted by one path and
# rejected by the next.
_MAX_PDF_DOM_UPLOAD_BYTES = 64 * 1024 * 1024
_PROVENANCE_ARCHIVE_MAX_BYTES = ARCHIVE_MAX_BYTES
ATTACHMENT_PARSE_BREAKDOWN_EVIDENCE_SOURCE = (
"email_attachments.content_type, "
"email_attachments.parse_content_type, "
Expand Down Expand Up @@ -3192,6 +3200,86 @@ def _quality_checks(
]


def _provenance_scope(auth_context: AuthContext) -> TenantProvenanceScope:
return TenantProvenanceScope(
user_id=auth_context.user_id,
organization_id=auth_context.organization_id,
workspace_id=auth_context.workspace_id,
)


def _require_authoritative_provenance_scope(auth_context: AuthContext) -> None:
if auth_context.session_verifier != "oidc":
raise HTTPException(
status_code=403,
detail="Authoritative workspace membership is required for provenance bundles",
)


async def _read_provenance_archive(request: Request) -> bytes:
content_length = request.headers.get("content-length")
if content_length is not None:
if not content_length.isdigit():
raise HTTPException(status_code=400, detail="Invalid Content-Length")
try:
declared_bytes = int(content_length)
except ValueError as exc:
raise HTTPException(
status_code=400, detail="Invalid Content-Length"
) from exc
if declared_bytes > _PROVENANCE_ARCHIVE_MAX_BYTES:
raise HTTPException(status_code=413, detail="Provenance archive too large")
archive = bytearray()
async for chunk in request.stream():
if len(chunk) > _PROVENANCE_ARCHIVE_MAX_BYTES - len(archive):
raise HTTPException(status_code=413, detail="Provenance archive too large")
archive.extend(chunk)
return bytes(archive)


@router.get("/provenance-bundle")
async def download_provenance_bundle(
auth_context: AuthContext = Depends(get_auth_context),
db: AsyncSession = Depends(get_db),
) -> Response:
_require_authoritative_provenance_scope(auth_context)
try:
archive = await export_tenant_provenance(db, _provenance_scope(auth_context))
except ProvenanceArchiveError as exc:
raise HTTPException(
status_code=400, detail="Invalid provenance archive"
) from exc
return Response(
content=archive,
media_type="application/zip",
headers={"Content-Disposition": 'attachment; filename="naruon-provenance.zip"'},
Comment thread
seonghobae marked this conversation as resolved.
)


@router.post("/provenance-bundle/import")
async def upload_provenance_bundle(
request: Request,
auth_context: AuthContext = Depends(get_auth_context),
db: AsyncSession = Depends(get_db),
) -> dict[str, object]:
_require_authoritative_provenance_scope(auth_context)
archive = await _read_provenance_archive(request)
try:
receipt = await import_tenant_provenance(
db, _provenance_scope(auth_context), archive
)
except ProvenanceArchiveError as exc:
raise HTTPException(
status_code=400, detail="Invalid provenance archive"
) from exc
return {
"bundle_uid": receipt.bundle_uid,
"manifest_digest": receipt.manifest_digest,
"created": receipt.created,
"skipped": receipt.skipped,
}


@router.post("/documents", response_model=DataDocumentActionResponse)
async def upload_data_document(
request: DataDocumentUploadRequest,
Expand Down
39 changes: 39 additions & 0 deletions backend/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1293,6 +1293,45 @@ class ProjectGraphCorrectionRecord(Base):
)


class ProvenanceIdentityMapping(Base):
__tablename__ = "provenance_identity_mappings"
__table_args__ = (
UniqueConstraint(
"target_user_id",
"target_organization_id",
"target_workspace_id",
"source_user_uid",
"source_organization_uid",
"source_workspace_uid",
"entity_kind",
"portable_uid",
name="uq_provenance_identity_source_target",
),
UniqueConstraint(
"entity_kind",
"target_database_uid",
name="uq_provenance_identity_target_uid",
),
Index(
"ix_provenance_identity_target_scope",
"target_user_id",
"target_organization_id",
"target_workspace_id",
),
)

provenance_identity_id: Mapped[int] = mapped_column(primary_key=True)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
target_user_id: Mapped[str] = mapped_column(String, nullable=False)
target_organization_id: Mapped[str] = mapped_column(String, nullable=False)
target_workspace_id: Mapped[str] = mapped_column(String, nullable=False)
source_user_uid: Mapped[str] = mapped_column(String(64), nullable=False)
source_organization_uid: Mapped[str] = mapped_column(String, nullable=False)
source_workspace_uid: Mapped[str] = mapped_column(String, nullable=False)
entity_kind: Mapped[str] = mapped_column(String(64), nullable=False)
portable_uid: Mapped[str] = mapped_column(String(256), nullable=False)
target_database_uid: Mapped[str] = mapped_column(String(96), nullable=False)
Comment thread
seonghobae marked this conversation as resolved.


class TenantConfig(Base):
__tablename__ = "tenant_configs"

Expand Down
Loading