Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
8dbeeb2
[SPEC] Agent-KV: agentic key-value extraction API — design document
arunvenkataswamy Aug 28, 2026
b6339ac
[SPEC] Agent-KV rev. b: amendments from adversarial feasibility review
arunvenkataswamy Aug 28, 2026
ba50af5
[PLAN] Agent-KV OSS half: 15-task TDD implementation plan
arunvenkataswamy Aug 28, 2026
59228e8
feat(agent-kv): shared keys.json schema compiler package with caps
arunvenkataswamy Aug 28, 2026
adc892e
feat(agent-kv): AGENT_KV file storage type with its own creds env
arunvenkataswamy Aug 28, 2026
315225e
feat(agent-kv): app scaffold, key/job models, terminal write guard
arunvenkataswamy Aug 28, 2026
a7d7118
test(agent-kv): pin org-scoped filter args in mark_terminal guard test
arunvenkataswamy Aug 28, 2026
593e45b
feat(agent-kv): org-scoped key management API with rotate
arunvenkataswamy Aug 28, 2026
3ff2d86
fix(agent-kv): own permission class + org-scoping mechanism test
arunvenkataswamy Aug 28, 2026
72b446b
feat(agent-kv): public mount, bearer-key validator, in-view auth
arunvenkataswamy Aug 28, 2026
6f6b68f
test(agent-kv): pin public URL wiring and decorator application
arunvenkataswamy Aug 28, 2026
9634301
feat(agent-kv): submit serializer enforcing every pre-paid-work cap
arunvenkataswamy Aug 28, 2026
06ad601
test(agent-kv): pin unreadable-PDF rejection and stream rewind
arunvenkataswamy Aug 28, 2026
9d5b6d0
feat(agent-kv): own-namespace concurrency limiter + per-key rate limit
arunvenkataswamy Aug 28, 2026
71c6930
test(agent-kv): pin limiter key namespaces and call ordering
arunvenkataswamy Aug 28, 2026
f3b7558
feat(agent-kv): staging, executor dispatch glue, submit view
arunvenkataswamy Aug 28, 2026
7829462
fix(agent-kv): release slot and terminalize on every submit failure path
arunvenkataswamy Aug 28, 2026
2b0f1b5
feat(agent-kv): status/result/cancel/delete with org-scoped 404s
arunvenkataswamy Aug 28, 2026
9c5c6c5
fix(agent-kv): serve failed/cancelled job results per spec §7.3
arunvenkataswamy Aug 28, 2026
3e37737
feat(agent-kv): free authenticated schema validation endpoint
arunvenkataswamy Aug 28, 2026
c1b6db0
feat(agent-kv): internal stage-report and finalize APIs (idempotent)
arunvenkataswamy Aug 28, 2026
a805b22
fix(agent-kv): race-safe stage merge, counters write-gate, malformed-…
arunvenkataswamy Aug 28, 2026
f907337
feat(agent-kv): terminal callbacks on a dedicated callback queue
arunvenkataswamy Aug 28, 2026
0cca794
fix(agent-kv): ide_callback worker consumes the agent_kv_callback queue
arunvenkataswamy Aug 28, 2026
5497dc4
fix(agent-kv): prefer transport-injected error; guard finalize networ…
arunvenkataswamy Aug 28, 2026
bfa3ecc
feat(agent-kv): SSRF-guarded webhook sender
arunvenkataswamy Aug 28, 2026
46f2ca5
test(agent-kv): pin IPv6, empty/multi-record resolution, non-2xx, all…
arunvenkataswamy Aug 28, 2026
bf0f4b7
feat(agent-kv): undispatched sweep and TTL cleanup internal endpoints
arunvenkataswamy Aug 28, 2026
b3601c7
fix(agent-kv): cap the undispatched sweep batch
arunvenkataswamy Aug 28, 2026
d2119a9
docs(agent-kv): API reference, env samples, deploy checklist
arunvenkataswamy Aug 28, 2026
ab1739b
fix(agent-kv): delete staged input when finalize terminalizes a job (…
arunvenkataswamy Aug 28, 2026
79a94fd
fix(agent-kv): final-review fixes — guarded dispatch stamp, orphan cl…
arunvenkataswamy Aug 28, 2026
d055057
feat(agent-kv): gate calculations and structured_output behind deploy…
arunvenkataswamy Aug 29, 2026
9f34593
feat(agent-kv): stage-report internal client; workers depend on the s…
arunvenkataswamy Aug 29, 2026
57f6f11
feat(agent-kv): management commands for sweep and TTL cleanup
arunvenkataswamy Aug 29, 2026
5ff1fe7
docs(agent-kv): concrete cloud deploy checklist
arunvenkataswamy Aug 29, 2026
c3b518f
docs(agent-kv): correct executor time-limit baseline and lever
arunvenkataswamy Aug 29, 2026
0787c96
test(agent-kv): e2e lane, rig group, and compose overlay for the inte…
arunvenkataswamy Aug 29, 2026
4c8f4e9
fix(agent-kv): return key on create; serialize the e2e concurrency test
arunvenkataswamy Aug 29, 2026
2145661
chore(agent-kv): re-lock root uv.lock for the schema package workspac…
arunvenkataswamy Aug 29, 2026
cbb7cc2
docs+test(agent-kv): cost_summary shape, rollout/time-limit notes, e2…
arunvenkataswamy Aug 29, 2026
f625064
fix(agent-kv): 13b integration fixes — platform key by org slug, buck…
arunvenkataswamy Aug 30, 2026
9c6f168
fix(agent-kv): make concurrency slot acquisition atomic (Lua script)
arunvenkataswamy Aug 30, 2026
db80544
fix(agent-kv): normalise AGENT_KV_STORAGE_DIR_PREFIX like the executo…
arunvenkataswamy Aug 30, 2026
30586da
test(agent-kv): live e2e coverage for webhook delivery, sync-wait and…
arunvenkataswamy Sep 1, 2026
ad9df39
docs(agent-kv): sandbox worker design spec (sub-project #2, S1-S12)
arunvenkataswamy Sep 1, 2026
059615c
docs(agent-kv): sandbox worker implementation plan (12 tasks)
arunvenkataswamy Sep 1, 2026
89011a8
feat(sandbox): register SANDBOX worker type, queue and task route
arunvenkataswamy Sep 1, 2026
d2e19e9
feat(sandbox): normative AST safety gate with adversarial tests
arunvenkataswamy Sep 1, 2026
41aab62
fix(sandbox): harden gate against builtins and attribute-form calls
arunvenkataswamy Sep 1, 2026
4fed021
fix(sandbox): switch import check to allowlist (closes module-bypass …
arunvenkataswamy Sep 1, 2026
d4023b7
fix(sandbox): reject any reference to dangerous builtins, not just di…
arunvenkataswamy Sep 1, 2026
07e3358
feat(sandbox): scrubbed-env rlimit subprocess runner with hostile-beh…
arunvenkataswamy Sep 1, 2026
b5c3d39
fix(sandbox): restrict sys to sys.argv only, closing the _getframe/se…
arunvenkataswamy Sep 1, 2026
7016f0a
fix(sandbox): close sys-aliasing gate bypass, scrub host paths, asser…
arunvenkataswamy Sep 1, 2026
c0a08fe
feat(sandbox): execute_sandboxed_code task + worker entrypoint with s…
arunvenkataswamy Sep 1, 2026
b4e285a
feat(sandbox): compose service, run-worker command and sample.env wiring
arunvenkataswamy Sep 1, 2026
79e55b1
docs(agent-kv): calculation result shape and sandbox rollout order
arunvenkataswamy Sep 1, 2026
8aa7013
test(agent-kv): e2e calculation happy-path and hostile scenarios (gated)
arunvenkataswamy Sep 1, 2026
5c40f67
fix(agent-kv): retarget hostile calc e2e off an accepted v1 residual
arunvenkataswamy Sep 1, 2026
822fa3c
fix(sandbox): run security tests in CI, allow re.compile, kill child …
arunvenkataswamy Sep 1, 2026
102c94d
docs(agent-kv): document the accepted v1 file-read residual (R12) + g…
arunvenkataswamy Sep 2, 2026
8f074fe
fix(agent-kv): pre-Greptile critical fixes — gate bypass, schema dept…
arunvenkataswamy Sep 2, 2026
04ee462
fix(agent-kv): pre-Greptile robustness + coverage + docs — runner/web…
arunvenkataswamy Sep 2, 2026
9d76701
docs(agent-kv): add architecture overview + handover index
arunvenkataswamy Sep 2, 2026
d11a2da
docs(agent-kv): note billing/metering status + push state in overview
arunvenkataswamy Sep 2, 2026
3f3dee1
docs(agent-kv): correct the R12 residual — pathlib is not in the AST …
arunvenkataswamy Sep 10, 2026
0695df5
Merge remote-tracking branch 'origin/main' into Feat/agent-kv-api
arunvenkataswamy Sep 10, 2026
3b67022
fix(agent-kv): give the agent-kv and sandbox queues a consumer on the…
arunvenkataswamy Sep 10, 2026
52a5355
feat(agent-kv): gate submits on the org's subscription, as API deploy…
arunvenkataswamy Sep 10, 2026
86c2ece
feat(agent-kv)!: extractor-scoped wire format — extractors[{name,keys…
arunvenkataswamy Sep 10, 2026
4a4b88c
test(agent-kv): cover the serializer<->view seam the wire-format chan…
arunvenkataswamy Sep 10, 2026
6c2c677
fix(agent-kv): two crashes a live submit found that no unit test could
arunvenkataswamy Sep 10, 2026
936ecee
fix(agent-kv): give agent_kv_callback a consumer on the PG fleet
arunvenkataswamy Sep 10, 2026
efc9964
refactor(agent-kv): derive SUPPORTED_EXTRACTORS from V1_EXTRACTOR_NAME
arunvenkataswamy Sep 10, 2026
b4c809d
test(agent-kv): fail loudly when an e2e submit drops extractor options
arunvenkataswamy Sep 10, 2026
e45ac6d
fix(agent-kv): address code-review findings on the wire-format change
arunvenkataswamy Sep 10, 2026
7fe7166
feat(agent-kv): route a table extractor entry to the table executor
arunvenkataswamy Sep 17, 2026
90076a4
fix(agent-kv): close two review gaps in the table-extractor routing
arunvenkataswamy Sep 17, 2026
8373b91
test(agent-kv): guard the table queue and cover the table extractor e2e
arunvenkataswamy Sep 17, 2026
a1f3e5a
UN-4044 docs(agent-kv): document the table extractor in the public AP…
arunvenkataswamy Sep 17, 2026
2824ab2
Merge remote-tracking branch 'origin/main' into agent-kv-api-rebase
vishnuszipstack Oct 1, 2026
a79e9d6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 1, 2026
eba836f
UN-4044 [FIX] Address Greptile review on the Agent-KV API PR
vishnuszipstack Oct 1, 2026
e8cd7d0
UN-4044 [MISC] Drop local dev scaffolding committed by mistake
vishnuszipstack Oct 1, 2026
cb6f7f2
UN-4044 [FIX] Stop failed TTL cleanups from starving the backlog
vishnuszipstack Oct 1, 2026
10969bd
UN-4044 [FIX] Stranded jobs, the TTL starvation inversion, and a thir…
vishnuszipstack Oct 4, 2026
c4276bb
UN-4044 [FIX] Refuse image uploads, cover three orphaned test dirs, c…
vishnuszipstack Oct 4, 2026
dcc0c43
UN-4044 [FIX] Fail the limiters closed, refuse ReDoS patterns, bound …
vishnuszipstack Oct 4, 2026
ffe96d2
UN-4044 [FIX] Tolerant equality in constraints; correct compile.py's …
vishnuszipstack Oct 4, 2026
0e1f29b
UN-4044 [MISC] Point the deferred-work comments at the tickets that e…
vishnuszipstack Oct 4, 2026
675d3a3
UN-4044 [FIX] Address the re-review of my own review fixes
vishnuszipstack Oct 4, 2026
63b3a91
UN-4044 [FIX] Mock the rate limiter in the validate-view tests
vishnuszipstack Oct 4, 2026
b752298
UN-4044 [FIX] Use math.fsum in the sum/avg aggregates
vishnuszipstack Oct 5, 2026
69182b5
UN-4044 [MISC] Clear 29 of the 39 SonarCloud issues on this PR
vishnuszipstack Oct 5, 2026
6bb4119
UN-4044 [MISC] Finish the S5778 hoist in test_auth — mock.Mock() was …
vishnuszipstack Oct 5, 2026
24d22c8
UN-4044 [FEAT] Make array row de-duplication opt-out per array
vishnuszipstack Oct 5, 2026
f05aaa0
UN-4044 [FIX] Pin the worker image UID so a pod can assert it
vishnuszipstack Oct 5, 2026
57bd3c6
Merge remote-tracking branch 'origin/main' into Feat/agent-kv-api
vishnuszipstack Oct 6, 2026
b962641
UN-4044 [MISC] Clear the 10 remaining SonarCloud issues on this PR
vishnuszipstack Oct 6, 2026
29b15fa
UN-4044 [FIX] Stop the WFE index-planner fixture tripping on float8 t…
vishnuszipstack Oct 6, 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
Empty file added backend/agent_kv/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions backend/agent_kv/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class AgentKvConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "agent_kv"
52 changes: 52 additions & 0 deletions backend/agent_kv/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#: v1 accepts exactly one extractor (`kv`), so the job row does not carry which
#: one it ran. When fan-out lands this becomes per-job state rather than a
#: constant -- the wire format (spec §7.0) is already shaped for that.
V1_EXTRACTOR_NAME = "kv"

STAGE_NAMES = [
"document_processing",
"extraction",
"qa",
"challenge",
"normalize",
"constraints",
"codegen",
"code_execution",
]
EXECUTOR_NAME = "agentic_kv"
OPERATION_KV_EXTRACT = "kv_extract"
EXECUTION_SOURCE = "agent_kv_api"

#: The table extractor, served by the cloud `agentic_table` plugin's blind-API
#: operation. Same executor as the IDE table path (and therefore the same,
#: already-wired `celery_executor_agentic_table` queue) with a second operation
#: -- a new executor name would derive a new queue needing wiring at five
#: sites, and an unwired queue accepts work and drains nothing, silently.
TABLE_EXTRACTOR_NAME = "table"

TABLE_EXECUTOR_NAME = "agentic_table"
OPERATION_TABLE_EXTRACT_API = "table_extract_api"

#: Which executor and operation each extractor dispatches to. `dispatch_job`
#: reads this rather than hardcoding one pair, so adding an extractor is one
#: entry here plus its options serializer and stage list.
EXTRACTOR_ROUTES = {
V1_EXTRACTOR_NAME: (EXECUTOR_NAME, OPERATION_KV_EXTRACT),
TABLE_EXTRACTOR_NAME: (TABLE_EXECUTOR_NAME, OPERATION_TABLE_EXTRACT_API),
}

#: The table engine reports one coarse stage: it has no node-level progress
#: hooks (the IDE path gets `stream_log` only), so inventing finer stages here
#: would describe progress the executor cannot actually report.
TABLE_STAGE_NAMES = ["table_extraction"]

#: Stage names ARE wire format -- they are returned to clients -- and they are
#: extractor-specific (`qa`/`challenge`/`codegen` mean nothing to the table
#: extractor). `_status_document` filters a job's recorded stages through the
#: list for the extractor that ran: `StageReportView` persists whatever name
#: the executor sends, so without a per-extractor list a table job's stages
#: would be stored and then silently filtered out of every status response.
STAGE_NAMES_BY_EXTRACTOR = {
V1_EXTRACTOR_NAME: STAGE_NAMES,
TABLE_EXTRACTOR_NAME: TABLE_STAGE_NAMES,
}
158 changes: 158 additions & 0 deletions backend/agent_kv/dispatch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""Executor dispatch glue (spec §5.3). One dispatch per job; UUID task_id."""

import logging
import uuid

from celery import signature
from django.conf import settings
from django.utils import timezone

from agent_kv.constants import EXECUTION_SOURCE, EXTRACTOR_ROUTES
from agent_kv.models import AgentKVJob, JobStatus
from unstract.sdk1.execution.context import ExecutionContext

logger = logging.getLogger(__name__)

CALLBACK_QUEUE = "agent_kv_callback"


class DispatchError(Exception):
"""Enqueue failed; the caller terminalizes the job (spec §5.3)."""


def _dispatcher():
# No `celery_app`: UN-4046 removed that parameter when the routing
# dispatcher's Celery branch went with the pg_queue_enabled flag. Passing it
# raises TypeError, so every submit failed to dispatch -- and because it
# fails at the call rather than at import, nothing catches it until a real
# request is made.
from pg_queue.executor_rpc import get_executor_dispatcher

return get_executor_dispatcher()


def _platform_api_key(job) -> str:
# Lazy import: avoids Django app registry init order (mirrors
# PromptStudioHelper._get_platform_api_key).
from platform_settings_v2.platform_auth_service import (
PlatformAuthenticationService,
)

# ``get_active_platform_key`` takes the org's public *slug*
# (``Organization.organization_id``, e.g. ``org_abc123``) and resolves it
# via ``get_organization_by_org_id`` -- NOT the row's UUID primary key that
# ``job.organization_id`` holds. Passing the PK here silently resolves to
# no organization and every dispatch fails with ``ActiveKeyNotFound``
# (caught live in the Task 13b integration run).
org_slug = job.organization.organization_id
platform_key = PlatformAuthenticationService.get_active_platform_key(org_slug)
if not platform_key:
raise DispatchError(f"No active platform key for org {org_slug}")
return str(platform_key.key)


def dispatch_job(job, *, extractor: str, schema: dict, options: dict) -> None:
executor_name, operation = EXTRACTOR_ROUTES[extractor]
org_id = str(job.organization_id)
# Everything that can fail — platform-key lookup, context construction,
# and the enqueue call itself — lives inside this try so no internal
# failure (e.g. a transient DB error resolving the platform key) can
# escape as a raw, uncaught exception. Only the post-success bookkeeping
# below runs outside it.
try:
job.task_id = uuid.uuid4()
context = ExecutionContext(
executor_name=executor_name,
operation=operation,
run_id=str(job.id),
execution_source=EXECUTION_SOURCE,
organization_id=org_id,
executor_params={
"job_id": str(job.id),
"input_ref": job.input_ref,
"schema": schema,
"options": options,
"platform_api_key": _platform_api_key(job),
# The CAP the engine must enforce (spec §6.1/§6.6), not the
# measured count -- job.pages_total is None for Excel (no
# pre-OCR page concept), which would otherwise leave the
# engine with nothing to check the post-OCR virtual-page cap
# against. The measured count still rides along separately.
"max_pages": settings.AGENT_KV_MAX_PAGES,
"pages_total": job.pages_total,
},
)
cb_kwargs = {"callback_kwargs": {"job_id": str(job.id), "org_id": org_id}}
_dispatcher().dispatch_with_callback(
context,
on_success=signature(
"agent_kv_complete", kwargs=cb_kwargs, queue=CALLBACK_QUEUE
),
on_error=signature("agent_kv_error", kwargs=cb_kwargs, queue=CALLBACK_QUEUE),
task_id=str(job.task_id),
)
except DispatchError:
raise
except Exception as e:
raise DispatchError(str(e)) from e
job.status = JobStatus.DISPATCHED
job.dispatched_at = timezone.now()
# Guarded queryset UPDATE, not job.save(): a plain save would blindly
# overwrite whatever status this job already raced to. Concretely: the
# executor can fail (or the job be cancelled) essentially instantly
# after enqueue, and its finalize callback can land -- marking the row
# FAILED/CANCELLED -- before this post-enqueue bookkeeping runs. An
# unconditional save() here would rewrite that terminal status back to
# DISPATCHED, un-terminalizing the job forever (nothing else ever
# revisits a DISPATCHED row). Only a still-PENDING row is advanced; a
# row this UPDATE doesn't match is left exactly as the winning writer
# left it. `modified_at` is stamped automatically by
# BaseModelQuerySet.update() (utils/models/base_model.py).
# Everything below is POST-ENQUEUE bookkeeping. The task is already on the
# queue, so a failure here must never be reported as a failed dispatch:
# `SubmitView` turns a DispatchError into a FAILED job, and the executor
# would then run, callback, and find a terminal row it cannot write to --
# the caller told nothing was billed for work that did run. Wrapped rather
# than left to propagate, which is what the single pre-review UPDATE did.
#
# Losing the bookkeeping entirely is recoverable: sweep phase 1 reaps a
# still-PENDING row with no `dispatched_at`, and phase 2's
# `dispatched_at IS NULL` arm covers the non-PENDING case.
try:
_record_dispatch(job)
except Exception:
logger.exception(
"agent-kv: dispatch bookkeeping failed for job %s after enqueue "
"(task is queued; the sweep will reconcile)",
job.id,
)


def _record_dispatch(job) -> None:
"""Persist task_id/status/dispatched_at against whatever the row raced to."""
advanced = AgentKVJob.objects.filter(id=job.id, status=JobStatus.PENDING).update(
task_id=job.task_id,
status=job.status,
dispatched_at=job.dispatched_at,
)
if not advanced:
# The row moved off PENDING between the enqueue above and this write.
# The benign case is a terminal status (the guard's whole purpose) --
# but there is a non-terminal one: StageReportView promotes
# PENDING -> RUNNING on the executor's FIRST stage report, which can
# easily land before this bookkeeping. The guard above then matches 0
# rows and `dispatched_at` stays NULL -- and a non-terminal row with a
# NULL `dispatched_at` is invisible to BOTH sweep phases: phase 1
# requires `status=PENDING`, phase 2 filters `dispatched_at__lt=cutoff`
# and SQL `NULL < x` is never true. The job reports `running` forever
# and `GET result` 409s for the life of the row, with nothing able to
# recover it.
#
# So stamp the dispatch bookkeeping for any still-non-terminal row,
# WITHOUT touching `status`: the row genuinely was dispatched, and
# moving RUNNING back to DISPATCHED would lose the executor's own
# progress. `dispatched_at__isnull=True` keeps this idempotent and
# stops a retry overwriting the original dispatch time.
AgentKVJob.objects.filter(id=job.id, dispatched_at__isnull=True).exclude(
status__in=list(AgentKVJob.TERMINAL)
).update(task_id=job.task_id, dispatched_at=job.dispatched_at)
Comment thread
vishnuszipstack marked this conversation as resolved.
16 changes: 16 additions & 0 deletions backend/agent_kv/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from rest_framework.exceptions import APIException


class EngineUnavailable(APIException):
status_code = 501
default_detail = "agent-kv engine not available on this deployment"


class RateLimited(APIException):
status_code = 429
default_detail = "Too many requests"


class JobNotFound(APIException):
status_code = 404
default_detail = "Job not found"
Loading
Loading