engineering-platform 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- engineering_platform/ENGINEERING_PLATFORM_CONFIG.json +32 -0
- engineering_platform/ENGINEERING_PLATFORM_VERSION.json +15 -0
- engineering_platform/__init__.py +1 -0
- engineering_platform/__main__.py +7 -0
- engineering_platform/agent_state.py +530 -0
- engineering_platform/agent_trust.py +174 -0
- engineering_platform/assets/dashboard.css +1317 -0
- engineering_platform/assets/dashboard.js +8534 -0
- engineering_platform/assets/dashboard_locales.mjs +4049 -0
- engineering_platform/assets/dashboard_status_store.mjs +41 -0
- engineering_platform/assets/operations-console/apple-touch-icon-dark.png +0 -0
- engineering_platform/assets/operations-console/apple-touch-icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-dark.png +0 -0
- engineering_platform/assets/operations-console/icon-light.png +0 -0
- engineering_platform/assets/operations-console/icon-transparent.png +0 -0
- engineering_platform/assets/operations-console/manifest.webmanifest +11 -0
- engineering_platform/capability_preflight.py +285 -0
- engineering_platform/capability_review.py +261 -0
- engineering_platform/central_data_transfer.py +195 -0
- engineering_platform/central_database.py +245 -0
- engineering_platform/central_store_migration.py +1672 -0
- engineering_platform/codex_capacity.py +81 -0
- engineering_platform/codex_chat.py +226 -0
- engineering_platform/codex_observability.py +153 -0
- engineering_platform/component_lock.py +40 -0
- engineering_platform/component_logging.py +420 -0
- engineering_platform/console_presentation.py +14 -0
- engineering_platform/console_route_ownership.py +83 -0
- engineering_platform/contracts/__init__.py +38 -0
- engineering_platform/contracts/ep_consumer.py +391 -0
- engineering_platform/contracts/models.py +105 -0
- engineering_platform/contracts/projection.py +401 -0
- engineering_platform/dashboard_browser_validation.py +206 -0
- engineering_platform/dashboard_state.py +630 -0
- engineering_platform/dashboard_supervisor.swift +105 -0
- engineering_platform/dashboard_translation.py +129 -0
- engineering_platform/dependabot_producer.py +349 -0
- engineering_platform/drift_diagnostics.py +144 -0
- engineering_platform/emergency_recovery.py +268 -0
- engineering_platform/engineering_memory.py +139 -0
- engineering_platform/ep_consumer_credentials.py +473 -0
- engineering_platform/evidence_projection.py +213 -0
- engineering_platform/execution_activity.py +218 -0
- engineering_platform/execution_context.py +132 -0
- engineering_platform/execution_errors.py +42 -0
- engineering_platform/execution_evidence.py +24 -0
- engineering_platform/execution_executor.py +730 -0
- engineering_platform/execution_finalization.py +44 -0
- engineering_platform/execution_host.py +3306 -0
- engineering_platform/execution_lease.py +365 -0
- engineering_platform/execution_lifecycle.py +447 -0
- engineering_platform/execution_models.py +43 -0
- engineering_platform/execution_readiness.py +166 -0
- engineering_platform/execution_reporting.py +1607 -0
- engineering_platform/execution_repository.py +253 -0
- engineering_platform/execution_timeout_policy.py +56 -0
- engineering_platform/execution_timing.py +440 -0
- engineering_platform/execution_transaction.py +28 -0
- engineering_platform/external_producer_binding.py +235 -0
- engineering_platform/file_inbox.py +249 -0
- engineering_platform/forensic_attribution.py +338 -0
- engineering_platform/forensic_attribution_v2.py +134 -0
- engineering_platform/forensic_delta.py +299 -0
- engineering_platform/golden_scenario.py +63 -0
- engineering_platform/historical_dashboard_configuration.py +171 -0
- engineering_platform/host_admin.py +199 -0
- engineering_platform/host_preflight.py +231 -0
- engineering_platform/installation_relocation.py +122 -0
- engineering_platform/investigation_ledger.py +89 -0
- engineering_platform/legacy_inbox_migration.py +79 -0
- engineering_platform/lifecycle_worker.py +223 -0
- engineering_platform/live_status.py +267 -0
- engineering_platform/local_api.py +209 -0
- engineering_platform/local_api_keychain.py +51 -0
- engineering_platform/local_repository_binding.py +138 -0
- engineering_platform/managed_autonomy.py +509 -0
- engineering_platform/managed_codex_runtime.py +105 -0
- engineering_platform/parity_context.py +203 -0
- engineering_platform/parity_lifecycle_dispatcher.py +488 -0
- engineering_platform/platform_admin.py +13 -0
- engineering_platform/platform_api.py +428 -0
- engineering_platform/platform_bootstrap.py +385 -0
- engineering_platform/platform_components.py +65 -0
- engineering_platform/platform_version.py +171 -0
- engineering_platform/pr_check_repair.py +276 -0
- engineering_platform/pr_evidence_backfill.py +278 -0
- engineering_platform/producer.py +209 -0
- engineering_platform/project_agent.py +366 -0
- engineering_platform/project_agent_service.py +244 -0
- engineering_platform/project_topology.py +126 -0
- engineering_platform/prompt_history.py +591 -0
- engineering_platform/provider_context.py +136 -0
- engineering_platform/provider_context_benchmark.py +41 -0
- engineering_platform/provider_context_scope.py +90 -0
- engineering_platform/provider_interruption.py +168 -0
- engineering_platform/provider_process_identity.py +80 -0
- engineering_platform/provider_readiness.py +138 -0
- engineering_platform/provider_recovery.py +647 -0
- engineering_platform/provider_usage.py +497 -0
- engineering_platform/providers.py +471 -0
- engineering_platform/qualification.py +220 -0
- engineering_platform/recommendation_handoff.py +238 -0
- engineering_platform/report_analysis.py +193 -0
- engineering_platform/repository_attachment.py +171 -0
- engineering_platform/repository_handoff.py +95 -0
- engineering_platform/resources.py +38 -0
- engineering_platform/reviewer_evidence.py +70 -0
- engineering_platform/schemas/repository-attachment.schema.json +61 -0
- engineering_platform/server.py +3679 -0
- engineering_platform/server_console_services.py +2024 -0
- engineering_platform/server_relay.py +172 -0
- engineering_platform/server_service.py +122 -0
- engineering_platform/status_model.py +135 -0
- engineering_platform/status_reconciliation.py +34 -0
- engineering_platform/storage.py +2440 -0
- engineering_platform/submission_cli.py +77 -0
- engineering_platform/submission_intake.py +45 -0
- engineering_platform/submission_service.py +317 -0
- engineering_platform/telemetry.py +951 -0
- engineering_platform/templates/workspace-config.json +25 -0
- engineering_platform/validation_identity.py +50 -0
- engineering_platform/validation_profile.py +211 -0
- engineering_platform/workspace_preflight.py +263 -0
- engineering_platform/worktree_provenance.py +147 -0
- engineering_platform/worktree_tooling.py +18 -0
- engineering_platform-2.2.0.dist-info/METADATA +18 -0
- engineering_platform-2.2.0.dist-info/RECORD +130 -0
- engineering_platform-2.2.0.dist-info/WHEEL +5 -0
- engineering_platform-2.2.0.dist-info/entry_points.txt +6 -0
- engineering_platform-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Installed HTTP consumer CLI for canonical submissions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from urllib.error import HTTPError, URLError
|
|
9
|
+
from urllib.request import Request, urlopen
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv: list[str] | None = None) -> int:
|
|
13
|
+
parser = argparse.ArgumentParser(prog="engineering-platform")
|
|
14
|
+
parser.add_argument("command", choices=("submit",))
|
|
15
|
+
parser.add_argument("--server", required=True, help="CENTRAL base URL")
|
|
16
|
+
parser.add_argument("--project", required=True)
|
|
17
|
+
parser.add_argument("--repository", required=True)
|
|
18
|
+
parser.add_argument("--producer-id", default="cli")
|
|
19
|
+
parser.add_argument("--producer-type", default="HUMAN")
|
|
20
|
+
parser.add_argument("--producer-version")
|
|
21
|
+
parser.add_argument("--prompt-file", required=True, type=Path)
|
|
22
|
+
parser.add_argument("--idempotency-key")
|
|
23
|
+
parser.add_argument("--correlation-id")
|
|
24
|
+
parser.add_argument("--mission-id")
|
|
25
|
+
parser.add_argument("--engineering-action-id")
|
|
26
|
+
parser.add_argument("--constraints-file", type=Path, help="JSON object for canonical submission constraints")
|
|
27
|
+
parser.add_argument("--credential-env", default="EP_CONSUMER_TOKEN")
|
|
28
|
+
args = parser.parse_args(argv)
|
|
29
|
+
token = os.environ.get(args.credential_env)
|
|
30
|
+
if not token:
|
|
31
|
+
parser.error(f"{args.credential_env} is required")
|
|
32
|
+
try:
|
|
33
|
+
prompt = args.prompt_file.read_text(encoding="utf-8")
|
|
34
|
+
except OSError as error:
|
|
35
|
+
parser.error(str(error))
|
|
36
|
+
constraints: object | None = None
|
|
37
|
+
if args.constraints_file is not None:
|
|
38
|
+
try:
|
|
39
|
+
constraints = json.loads(args.constraints_file.read_text(encoding="utf-8"))
|
|
40
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
41
|
+
parser.error(f"invalid constraints file: {error}")
|
|
42
|
+
if not isinstance(constraints, dict):
|
|
43
|
+
parser.error("constraints file must contain a JSON object")
|
|
44
|
+
payload: dict[str, object] = {"repository_id": args.repository, "producer": {"id": args.producer_id, "type": args.producer_type}, "prompt": prompt}
|
|
45
|
+
if args.producer_version:
|
|
46
|
+
payload["producer"] = {**payload["producer"], "version": args.producer_version} # type: ignore[arg-type]
|
|
47
|
+
if args.idempotency_key:
|
|
48
|
+
payload["idempotency_key"] = args.idempotency_key
|
|
49
|
+
for field in ("correlation_id", "mission_id", "engineering_action_id"):
|
|
50
|
+
value = getattr(args, field)
|
|
51
|
+
if value:
|
|
52
|
+
payload[field] = value
|
|
53
|
+
if constraints is not None:
|
|
54
|
+
payload["constraints"] = constraints
|
|
55
|
+
request = Request(
|
|
56
|
+
args.server.rstrip("/") + f"/v1/projects/{args.project}/submissions",
|
|
57
|
+
data=json.dumps(payload).encode("utf-8"), method="POST",
|
|
58
|
+
headers={
|
|
59
|
+
"Content-Type": "application/json",
|
|
60
|
+
"Authorization": f"Bearer {token}",
|
|
61
|
+
"EP-Submission-Transport": "CLI",
|
|
62
|
+
},
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
with urlopen(request, timeout=15) as response: # nosec B310 -- operator supplied loopback CENTRAL URL
|
|
66
|
+
print(response.read().decode("utf-8"))
|
|
67
|
+
except HTTPError as error:
|
|
68
|
+
print(error.read().decode("utf-8"))
|
|
69
|
+
return 1
|
|
70
|
+
except URLError as error:
|
|
71
|
+
print(json.dumps({"error": "CENTRAL_UNAVAILABLE"}))
|
|
72
|
+
return 1
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Deterministic, transport-neutral Human Intent normalization.
|
|
2
|
+
|
|
3
|
+
This module parses explicit metadata only. It never selects a repository,
|
|
4
|
+
opens storage, dispatches a run, or interprets product intent with an AI.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SubmissionIntakeError(ValueError):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalize_human_file(path: Path) -> dict[str, object]:
|
|
16
|
+
try:
|
|
17
|
+
text = path.read_text(encoding="utf-8")
|
|
18
|
+
except UnicodeDecodeError as error:
|
|
19
|
+
raise SubmissionIntakeError("UNSUPPORTED_ENCODING") from error
|
|
20
|
+
if not text.strip() or not text.startswith("---\n"):
|
|
21
|
+
raise SubmissionIntakeError("HUMAN_METADATA_REQUIRED")
|
|
22
|
+
try:
|
|
23
|
+
header, body = text[4:].split("\n---\n", 1)
|
|
24
|
+
except ValueError as error:
|
|
25
|
+
raise SubmissionIntakeError("MALFORMED_HUMAN_METADATA") from error
|
|
26
|
+
metadata: dict[str, str] = {}
|
|
27
|
+
for line in header.splitlines():
|
|
28
|
+
if ":" not in line:
|
|
29
|
+
raise SubmissionIntakeError("MALFORMED_HUMAN_METADATA")
|
|
30
|
+
key, value = (part.strip() for part in line.split(":", 1))
|
|
31
|
+
if key in metadata or key not in {"project", "repository", "mode", "target"} or not value:
|
|
32
|
+
raise SubmissionIntakeError("MALFORMED_HUMAN_METADATA")
|
|
33
|
+
metadata[key] = value
|
|
34
|
+
if set(metadata) - {"project", "repository", "mode", "target"} or not {"project", "repository", "mode"} <= set(metadata):
|
|
35
|
+
raise SubmissionIntakeError("HUMAN_METADATA_REQUIRED")
|
|
36
|
+
mode = metadata["mode"].upper()
|
|
37
|
+
if mode not in {"MANAGED", "GENESIS"} or (mode == "GENESIS") != ("target" in metadata):
|
|
38
|
+
raise SubmissionIntakeError("INVALID_HUMAN_MODE_OR_TARGET")
|
|
39
|
+
if not body.strip():
|
|
40
|
+
raise SubmissionIntakeError("EMPTY_HUMAN_INTENT")
|
|
41
|
+
prompt = f"Execution Mode: {mode}\n"
|
|
42
|
+
if mode == "GENESIS":
|
|
43
|
+
prompt += f"Target repository: {metadata['target']}\n"
|
|
44
|
+
prompt += "\n" + body.strip() + "\n"
|
|
45
|
+
return {"project_id": metadata["project"], "submission": {"repository_id": metadata["repository"], "producer": {"id": "file-human", "type": "HUMAN", "version": "submission-intake-v1"}, "prompt": prompt, "constraints": {"normalization": "submission-intake-v1", "mode": mode}}}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""Canonical, transport-neutral CENTRAL submission intake.
|
|
2
|
+
|
|
3
|
+
Adapters only turn their input into :class:`SubmissionRequest`; this module is
|
|
4
|
+
the single owner of project resolution, durable acceptance, idempotency and
|
|
5
|
+
the initial admission/queue projection. It intentionally does *not* execute
|
|
6
|
+
a provider.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import secrets
|
|
15
|
+
import sqlite3
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Mapping
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MAX_PROMPT_BYTES = 65536
|
|
21
|
+
MAX_FIELD_LENGTH = 128
|
|
22
|
+
MAX_CONSTRAINT_BYTES = 8192
|
|
23
|
+
VALID_TRANSPORTS = frozenset({"HTTP", "CLI", "FILE_INBOX", "DEPENDABOT", "LEGACY_FILE"})
|
|
24
|
+
VALID_EXECUTION_MODES = frozenset({"MANAGED", "GENESIS"})
|
|
25
|
+
|
|
26
|
+
# This is the complete B8D lifecycle. The final value deliberately says what
|
|
27
|
+
# CENTRAL has *not* done: admission makes a submission eligible for a later
|
|
28
|
+
# execution protocol, but it never invokes a provider or a local Codex binary.
|
|
29
|
+
SUBMISSION_LIFECYCLE_VERSION = "1"
|
|
30
|
+
SUBMISSION_ACCEPTED = "ACCEPTED"
|
|
31
|
+
ADMISSION_GRANTED = "ADMITTED"
|
|
32
|
+
EXECUTION_NOT_DISPATCHED = "NOT_DISPATCHED"
|
|
33
|
+
_LIFECYCLE_EVENT_KINDS = (
|
|
34
|
+
"SUBMISSION_ACCEPTED",
|
|
35
|
+
"ADMISSION_GRANTED",
|
|
36
|
+
"EXECUTION_NOT_DISPATCHED",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SubmissionError(ValueError):
|
|
41
|
+
"""A stable, safe submission rejection."""
|
|
42
|
+
def __init__(self, code: str, status: int = 400) -> None:
|
|
43
|
+
super().__init__(code)
|
|
44
|
+
self.code, self.status = code, status
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _lifecycle_payload(*, transport: str, producer_id: str) -> dict[str, str]:
|
|
48
|
+
"""Return the one durable lifecycle meaning shared by every adapter."""
|
|
49
|
+
return {
|
|
50
|
+
"version": SUBMISSION_LIFECYCLE_VERSION,
|
|
51
|
+
"submission": SUBMISSION_ACCEPTED,
|
|
52
|
+
"admission": ADMISSION_GRANTED,
|
|
53
|
+
"execution": EXECUTION_NOT_DISPATCHED,
|
|
54
|
+
"transport": transport,
|
|
55
|
+
"producer_id": producer_id,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class SubmissionRequest:
|
|
61
|
+
project_id: str
|
|
62
|
+
repository_id: str
|
|
63
|
+
producer_id: str
|
|
64
|
+
producer_type: str
|
|
65
|
+
producer_version: str | None
|
|
66
|
+
prompt: str
|
|
67
|
+
transport: str
|
|
68
|
+
idempotency_key: str | None = None
|
|
69
|
+
correlation_id: str | None = None
|
|
70
|
+
mission_id: str | None = None
|
|
71
|
+
engineering_action_id: str | None = None
|
|
72
|
+
constraints: Mapping[str, object] | None = None
|
|
73
|
+
transport_receipt_id: str | None = None
|
|
74
|
+
transport_received_at: str | None = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class SubmissionResult:
|
|
79
|
+
submission_id: str
|
|
80
|
+
project_id: str
|
|
81
|
+
repository_id: str
|
|
82
|
+
state: str
|
|
83
|
+
created_at: str
|
|
84
|
+
admission: str
|
|
85
|
+
transport: str
|
|
86
|
+
producer_id: str
|
|
87
|
+
duplicate: bool = False
|
|
88
|
+
|
|
89
|
+
def to_dict(self) -> dict[str, object]:
|
|
90
|
+
return {"submission_id": self.submission_id, "project_id": self.project_id,
|
|
91
|
+
"repository_id": self.repository_id, "state": self.state,
|
|
92
|
+
"created_at": self.created_at, "admission": self.admission,
|
|
93
|
+
"transport": self.transport, "duplicate": self.duplicate,
|
|
94
|
+
"lifecycle": _lifecycle_payload(
|
|
95
|
+
transport=self.transport,
|
|
96
|
+
producer_id=self.producer_id,
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _now() -> str:
|
|
101
|
+
return datetime.now(timezone.utc).isoformat()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _token(value: object, field: str, *, optional: bool = False) -> str | None:
|
|
105
|
+
if value is None and optional:
|
|
106
|
+
return None
|
|
107
|
+
if not isinstance(value, str) or not value or len(value) > MAX_FIELD_LENGTH or "\x00" in value:
|
|
108
|
+
raise SubmissionError(f"INVALID_{field.upper()}")
|
|
109
|
+
return value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _json_object(value: object) -> dict[str, object]:
|
|
113
|
+
if value is None:
|
|
114
|
+
return {}
|
|
115
|
+
if not isinstance(value, Mapping):
|
|
116
|
+
raise SubmissionError("INVALID_CONSTRAINTS")
|
|
117
|
+
try:
|
|
118
|
+
encoded = json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False).encode("utf-8")
|
|
119
|
+
except (TypeError, ValueError):
|
|
120
|
+
raise SubmissionError("INVALID_CONSTRAINTS") from None
|
|
121
|
+
if len(encoded) > MAX_CONSTRAINT_BYTES or b"\0" in encoded:
|
|
122
|
+
raise SubmissionError("INVALID_CONSTRAINTS")
|
|
123
|
+
return dict(value)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _transport(value: object) -> str:
|
|
127
|
+
if not isinstance(value, str) or value not in VALID_TRANSPORTS:
|
|
128
|
+
raise SubmissionError("INVALID_TRANSPORT")
|
|
129
|
+
return value
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _validate_execution_mode(request: SubmissionRequest) -> None:
|
|
133
|
+
"""Reject invalid mode declarations before CENTRAL writes admission state."""
|
|
134
|
+
declarations = {
|
|
135
|
+
line.split(":", 1)[1].strip().upper()
|
|
136
|
+
for line in request.prompt.splitlines()
|
|
137
|
+
if line.strip().lower().startswith("execution mode:")
|
|
138
|
+
}
|
|
139
|
+
if declarations and (not declarations <= VALID_EXECUTION_MODES or len(declarations) != 1):
|
|
140
|
+
raise SubmissionError("INVALID_EXECUTION_MODE")
|
|
141
|
+
if declarations == {"GENESIS"}:
|
|
142
|
+
targets = [line.split(":", 1)[1].strip() for line in request.prompt.splitlines() if line.strip().lower().startswith("target repository:")]
|
|
143
|
+
if len(set(targets)) != 1 or not targets[0] or not Path(targets[0]).is_absolute():
|
|
144
|
+
raise SubmissionError("INVALID_GENESIS_TARGET")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def request_from_mapping(project_id: str, payload: object, *, transport: str) -> SubmissionRequest:
|
|
148
|
+
if not isinstance(payload, Mapping):
|
|
149
|
+
raise SubmissionError("MALFORMED_REQUEST")
|
|
150
|
+
allowed = {"repository_id", "producer", "prompt", "idempotency_key", "correlation_id", "mission_id", "engineering_action_id", "constraints", "transport_receipt_id", "transport_received_at"}
|
|
151
|
+
if set(payload) - allowed:
|
|
152
|
+
raise SubmissionError("UNKNOWN_FIELD")
|
|
153
|
+
producer = payload.get("producer")
|
|
154
|
+
if not isinstance(producer, Mapping) or set(producer) - {"id", "type", "version"}:
|
|
155
|
+
raise SubmissionError("INVALID_PRODUCER")
|
|
156
|
+
prompt = payload.get("prompt")
|
|
157
|
+
if not isinstance(prompt, str) or not prompt.strip() or "\x00" in prompt or len(prompt.encode("utf-8")) > MAX_PROMPT_BYTES:
|
|
158
|
+
raise SubmissionError("INVALID_PROMPT")
|
|
159
|
+
return SubmissionRequest(
|
|
160
|
+
project_id=_token(project_id, "project_id") or "",
|
|
161
|
+
repository_id=_token(payload.get("repository_id"), "repository_id") or "",
|
|
162
|
+
producer_id=_token(producer.get("id"), "producer_id") or "",
|
|
163
|
+
producer_type=_token(producer.get("type"), "producer_type") or "",
|
|
164
|
+
producer_version=_token(producer.get("version"), "producer_version", optional=True),
|
|
165
|
+
prompt=prompt.replace("\r\n", "\n").replace("\r", "\n"), transport=_transport(transport),
|
|
166
|
+
idempotency_key=_token(payload.get("idempotency_key"), "idempotency_key", optional=True),
|
|
167
|
+
correlation_id=_token(payload.get("correlation_id"), "correlation_id", optional=True),
|
|
168
|
+
mission_id=_token(payload.get("mission_id"), "mission_id", optional=True),
|
|
169
|
+
engineering_action_id=_token(payload.get("engineering_action_id"), "engineering_action_id", optional=True),
|
|
170
|
+
constraints=_json_object(payload.get("constraints")),
|
|
171
|
+
transport_receipt_id=_token(payload.get("transport_receipt_id"), "transport_receipt_id", optional=True),
|
|
172
|
+
transport_received_at=_token(payload.get("transport_received_at"), "transport_received_at", optional=True),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _same_idempotent_request(row: tuple[object, ...], request: SubmissionRequest) -> bool:
|
|
177
|
+
"""Ensure an idempotency key cannot silently alias another submission."""
|
|
178
|
+
stored = {
|
|
179
|
+
"repository_id": row[0], "producer_id": row[1], "producer_type": row[2],
|
|
180
|
+
"producer_version": row[3], "prompt": row[4], "constraints": row[5],
|
|
181
|
+
"correlation_id": row[6], "mission_id": row[7],
|
|
182
|
+
"engineering_action_id": row[8], "transport_receipt_id": row[9],
|
|
183
|
+
}
|
|
184
|
+
requested = {
|
|
185
|
+
"repository_id": request.repository_id, "producer_id": request.producer_id,
|
|
186
|
+
"producer_type": request.producer_type, "producer_version": request.producer_version,
|
|
187
|
+
"prompt": request.prompt,
|
|
188
|
+
"constraints": json.dumps(request.constraints or {}, sort_keys=True),
|
|
189
|
+
"correlation_id": request.correlation_id, "mission_id": request.mission_id,
|
|
190
|
+
"engineering_action_id": request.engineering_action_id,
|
|
191
|
+
"transport_receipt_id": request.transport_receipt_id,
|
|
192
|
+
}
|
|
193
|
+
return stored == requested
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _persist_lifecycle_events(
|
|
197
|
+
connection: sqlite3.Connection,
|
|
198
|
+
*, submission_id: str,
|
|
199
|
+
transport: str,
|
|
200
|
+
producer_id: str,
|
|
201
|
+
recorded_at: str,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""Persist all three lifecycle boundaries in their canonical order."""
|
|
204
|
+
payload = json.dumps(
|
|
205
|
+
_lifecycle_payload(transport=transport, producer_id=producer_id), sort_keys=True,
|
|
206
|
+
)
|
|
207
|
+
for event_kind in _LIFECYCLE_EVENT_KINDS:
|
|
208
|
+
connection.execute(
|
|
209
|
+
"INSERT INTO ep_submission_events(submission_id,event_kind,payload,recorded_at) VALUES(?,?,?,?)",
|
|
210
|
+
(submission_id, event_kind, payload, recorded_at),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def lifecycle(connection: sqlite3.Connection, submission_id: str) -> dict[str, str]:
|
|
215
|
+
"""Read and validate the durable submission/admission/execution boundary.
|
|
216
|
+
|
|
217
|
+
This is an observation helper. It never performs dispatch and rejects a
|
|
218
|
+
partial or reordered event history rather than inventing lifecycle state.
|
|
219
|
+
"""
|
|
220
|
+
row = connection.execute(
|
|
221
|
+
"SELECT producer_id,transport,state,admission FROM ep_submissions WHERE submission_id=?",
|
|
222
|
+
(submission_id,),
|
|
223
|
+
).fetchone()
|
|
224
|
+
if row is None:
|
|
225
|
+
raise SubmissionError("UNKNOWN_SUBMISSION", 404)
|
|
226
|
+
events = [
|
|
227
|
+
str(event[0]) for event in connection.execute(
|
|
228
|
+
"SELECT event_kind FROM ep_submission_events WHERE submission_id=? ORDER BY event_id",
|
|
229
|
+
(submission_id,),
|
|
230
|
+
)
|
|
231
|
+
]
|
|
232
|
+
if tuple(events) != _LIFECYCLE_EVENT_KINDS or row[2:] != ("QUEUED", ADMISSION_GRANTED):
|
|
233
|
+
raise SubmissionError("SUBMISSION_LIFECYCLE_INCOMPLETE", 500)
|
|
234
|
+
return _lifecycle_payload(transport=str(row[1]), producer_id=str(row[0]))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def submit(connection: sqlite3.Connection, request: SubmissionRequest) -> SubmissionResult:
|
|
238
|
+
"""Persist and admit one request; no provider or Agent is selected here."""
|
|
239
|
+
_transport(request.transport)
|
|
240
|
+
_validate_execution_mode(request)
|
|
241
|
+
project = connection.execute("SELECT status FROM ep_project_registrations WHERE project_id=?", (request.project_id,)).fetchone()
|
|
242
|
+
if project is None:
|
|
243
|
+
raise SubmissionError("UNKNOWN_PROJECT", 404)
|
|
244
|
+
if project[0] != "ACTIVE":
|
|
245
|
+
raise SubmissionError("PROJECT_NOT_ACTIVE", 409)
|
|
246
|
+
repository = connection.execute("SELECT project_id FROM ep_repository_registrations WHERE repository_id=?", (request.repository_id,)).fetchone()
|
|
247
|
+
if repository is None:
|
|
248
|
+
raise SubmissionError("UNKNOWN_REPOSITORY", 404)
|
|
249
|
+
if repository[0] != request.project_id:
|
|
250
|
+
raise SubmissionError("REPOSITORY_PROJECT_CONFLICT", 409)
|
|
251
|
+
if request.idempotency_key:
|
|
252
|
+
duplicate = connection.execute(
|
|
253
|
+
"SELECT submission_id,created_at,state,admission,repository_id,producer_id,producer_type,producer_version,prompt,constraints,correlation_id,mission_id,engineering_action_id,transport_receipt_id,transport "
|
|
254
|
+
"FROM ep_submissions WHERE project_id=? AND idempotency_key=?",
|
|
255
|
+
(request.project_id, request.idempotency_key),
|
|
256
|
+
).fetchone()
|
|
257
|
+
if duplicate is not None:
|
|
258
|
+
if not _same_idempotent_request(tuple(duplicate[4:14]), request):
|
|
259
|
+
raise SubmissionError("IDEMPOTENCY_CONFLICT", 409)
|
|
260
|
+
lifecycle(connection, str(duplicate[0]))
|
|
261
|
+
return SubmissionResult(
|
|
262
|
+
str(duplicate[0]), request.project_id, str(duplicate[4]), str(duplicate[2]),
|
|
263
|
+
str(duplicate[1]), str(duplicate[3]), str(duplicate[14]), str(duplicate[5]), True,
|
|
264
|
+
)
|
|
265
|
+
submission_id, created_at = "sub-" + secrets.token_hex(16), _now()
|
|
266
|
+
# Admission intentionally validates CENTRAL topology only at submission
|
|
267
|
+
# time. Agent selection, leases and provider execution remain downstream.
|
|
268
|
+
admission, state = "ADMITTED", "QUEUED"
|
|
269
|
+
prompt_digest = hashlib.sha256(request.prompt.encode("utf-8")).hexdigest()
|
|
270
|
+
connection.execute("""INSERT INTO ep_submissions(submission_id,project_id,repository_id,producer_id,producer_type,producer_version,transport,prompt,prompt_digest,constraints,idempotency_key,correlation_id,mission_id,engineering_action_id,transport_receipt_id,transport_received_at,state,admission,created_at)
|
|
271
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", (submission_id, request.project_id, request.repository_id, request.producer_id, request.producer_type, request.producer_version, request.transport, request.prompt, prompt_digest, json.dumps(request.constraints or {}, sort_keys=True), request.idempotency_key, request.correlation_id, request.mission_id, request.engineering_action_id, request.transport_receipt_id, request.transport_received_at, state, admission, created_at))
|
|
272
|
+
_persist_lifecycle_events(
|
|
273
|
+
connection, submission_id=submission_id, transport=request.transport,
|
|
274
|
+
producer_id=request.producer_id, recorded_at=created_at,
|
|
275
|
+
)
|
|
276
|
+
connection.execute("INSERT INTO ep_submission_prompt_history(submission_id,prompt_digest,recorded_at) VALUES(?,?,?)", (submission_id, prompt_digest, created_at))
|
|
277
|
+
lifecycle(connection, submission_id)
|
|
278
|
+
return SubmissionResult(
|
|
279
|
+
submission_id, request.project_id, request.repository_id, state, created_at,
|
|
280
|
+
admission, request.transport, request.producer_id,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def register_consumer(connection: sqlite3.Connection, *, consumer_id: str, project_id: str) -> None:
|
|
285
|
+
"""Explicitly grant a consumer one project scope in CENTRAL."""
|
|
286
|
+
consumer_id = _token(consumer_id, "consumer_id") or ""
|
|
287
|
+
project_id = _token(project_id, "project_id") or ""
|
|
288
|
+
if connection.execute("SELECT 1 FROM ep_project_registrations WHERE project_id=? AND status='ACTIVE'", (project_id,)).fetchone() is None:
|
|
289
|
+
raise SubmissionError("UNKNOWN_PROJECT", 404)
|
|
290
|
+
now = _now()
|
|
291
|
+
connection.execute("""INSERT INTO ep_consumer_registrations(consumer_id,project_id,status,created_at,updated_at,audit_metadata)
|
|
292
|
+
VALUES(?,?,'ACTIVE',?,?,?) ON CONFLICT(consumer_id,project_id) DO UPDATE SET status='ACTIVE',updated_at=excluded.updated_at""", (consumer_id, project_id, now, now, json.dumps({"action": "SUBMISSION_CONSUMER_REGISTER"}, sort_keys=True)))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def issue_consumer_credential(connection: sqlite3.Connection, *, consumer_id: str, project_id: str) -> dict[str, str]:
|
|
296
|
+
"""Issue a scoped bearer token once; only its verifier is retained."""
|
|
297
|
+
register_consumer(connection, consumer_id=consumer_id, project_id=project_id)
|
|
298
|
+
from .ep_consumer_credentials import fingerprint, verifier
|
|
299
|
+
token, credential_id, now = secrets.token_urlsafe(32), "production-" + secrets.token_hex(16), _now()
|
|
300
|
+
connection.execute("INSERT INTO ep_consumer_credentials(credential_id,consumer_id,project_id,verifier,fingerprint,issued_at) VALUES(?,?,?,?,?,?)", (credential_id, consumer_id, project_id, verifier(token), fingerprint(token), now))
|
|
301
|
+
return {"credential_id": credential_id, "consumer_id": consumer_id, "project_id": project_id, "credential": token}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def submit_legacy_file(connection: sqlite3.Connection, path: object) -> SubmissionResult:
|
|
305
|
+
"""Compatibility adapter: a bounded JSON envelope with explicit project scope."""
|
|
306
|
+
from pathlib import Path
|
|
307
|
+
candidate = Path(path)
|
|
308
|
+
raw = candidate.read_bytes()
|
|
309
|
+
if len(raw) > 131072 or b"\0" in raw:
|
|
310
|
+
raise SubmissionError("MALFORMED_LEGACY_FILE")
|
|
311
|
+
try:
|
|
312
|
+
envelope = json.loads(raw.decode("utf-8"))
|
|
313
|
+
except (UnicodeDecodeError, json.JSONDecodeError):
|
|
314
|
+
raise SubmissionError("MALFORMED_LEGACY_FILE") from None
|
|
315
|
+
if not isinstance(envelope, Mapping) or set(envelope) != {"project_id", "submission"}:
|
|
316
|
+
raise SubmissionError("MALFORMED_LEGACY_FILE")
|
|
317
|
+
return submit(connection, request_from_mapping(str(envelope["project_id"]), envelope["submission"], transport="LEGACY_FILE"))
|