This commit is contained in:
1 parent
34f4923566
commit
146aa3a281
3 files changed
+98
-10
No files matched your search
+1
-1
Submodule agents/gewu updated: 65bc8a9422...5a6cd7af4f.
@@ -1285,10 +1285,10 @@ def create_mcp_server(
|
||||
|
||||
@server.tool(name="proposal_submit", structured_output=False)
|
||||
def proposal_submit(proposal_json: str, ctx: Context) -> str:
|
||||
"""Validate and submit one bounded typed proposal revision."""
|
||||
"""Validate and submit one bounded proposal; report any safe input repair."""
|
||||
|
||||
principal = _principal(ctx, identity_provider)
|
||||
proposal = _parse_proposal(proposal_json)
|
||||
proposal, input_repaired = _parse_proposal(proposal_json)
|
||||
context = context_provider()
|
||||
_authorize_proposal(principal, proposal, context)
|
||||
staged_extractions: dict[str, ExtractionRecord] = {}
|
||||
@@ -1321,6 +1321,7 @@ def create_mcp_server(
|
||||
"status": "accepted" if outcome.receipt else outcome.validation.status,
|
||||
"validation_status": outcome.validation.status,
|
||||
"eligible_for_auto_accept": outcome.validation.eligible_for_auto_accept,
|
||||
"input_repaired": input_repaired,
|
||||
"findings": [
|
||||
finding.model_dump(mode="json") for finding in outcome.validation.findings
|
||||
],
|
||||
@@ -1339,8 +1340,20 @@ def create_mcp_server(
|
||||
raise ValueError("Proposal exceeds the supported request size")
|
||||
try:
|
||||
draft = json.loads(proposal_json, object_pairs_hook=_reject_duplicate_keys)
|
||||
except (ValueError, TypeError) as error:
|
||||
raise ValueError("Proposal draft must be valid JSON") from error
|
||||
except (ValueError, TypeError):
|
||||
return json.dumps(
|
||||
{
|
||||
"status": "invalid",
|
||||
"issues": [
|
||||
{
|
||||
"path": "",
|
||||
"code": "invalid_proposal_draft",
|
||||
"message": "Proposal draft must be valid JSON",
|
||||
}
|
||||
],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
if not isinstance(draft, dict):
|
||||
raise ValueError("Proposal draft must be a JSON object")
|
||||
try:
|
||||
@@ -1378,10 +1391,36 @@ def create_mcp_server(
|
||||
|
||||
@server.tool(name="proposal_evaluate", structured_output=False)
|
||||
def proposal_evaluate(proposal_json: str, ctx: Context) -> str:
|
||||
"""Evaluate one bounded proposal without storing or publishing it."""
|
||||
"""Evaluate one bounded proposal without storing or publishing it.
|
||||
|
||||
A single missing final outer brace is repaired only if the result
|
||||
is valid JSON and matches the full contract. The response exposes
|
||||
input_repaired=true. Other malformed JSON returns status=invalid.
|
||||
A valid proposal may instead return needs-review when an applicable
|
||||
policy requires an owner decision.
|
||||
"""
|
||||
|
||||
principal = _principal(ctx, identity_provider)
|
||||
proposal = _parse_proposal(proposal_json)
|
||||
try:
|
||||
proposal, input_repaired = _parse_proposal(proposal_json)
|
||||
except ValueError:
|
||||
return json.dumps(
|
||||
{
|
||||
"proposal_id": None,
|
||||
"revision": None,
|
||||
"status": "invalid",
|
||||
"eligible_for_auto_accept": False,
|
||||
"findings": [
|
||||
{
|
||||
"code": "PROPOSAL_JSON_INVALID",
|
||||
"message": "Proposal does not match the GEWU v1 contract",
|
||||
"path": "proposal_json",
|
||||
}
|
||||
],
|
||||
"affected_policy_ids": [],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
context = context_provider()
|
||||
_authorize_proposal(principal, proposal, context)
|
||||
staged_extractions: dict[str, ExtractionRecord] = {}
|
||||
@@ -1408,6 +1447,7 @@ def create_mcp_server(
|
||||
"revision": proposal.revision,
|
||||
"status": report.status,
|
||||
"eligible_for_auto_accept": report.eligible_for_auto_accept,
|
||||
"input_repaired": input_repaired,
|
||||
"findings": [finding.model_dump(mode="json") for finding in report.findings],
|
||||
"affected_policy_ids": report.affected_policy_ids,
|
||||
},
|
||||
@@ -1673,17 +1713,32 @@ def _reject_duplicate_keys(pairs: list[tuple[str, object]]) -> dict[str, object]
|
||||
return result
|
||||
|
||||
|
||||
def _parse_proposal(proposal_json: str) -> ProposalRevision:
|
||||
def _parse_proposal(proposal_json: str) -> tuple[ProposalRevision, bool]:
|
||||
if len(proposal_json.encode("utf-8")) > 1_000_000:
|
||||
raise ValueError("Proposal exceeds the supported request size")
|
||||
try:
|
||||
parsed = json.loads(proposal_json, object_pairs_hook=_reject_duplicate_keys)
|
||||
proposal = ProposalRevision.model_validate_json(proposal_json, strict=True)
|
||||
except json.JSONDecodeError as error:
|
||||
# Tool-call argument serialization can drop the final outer brace while
|
||||
# copying a prepared JSON string. Accept only this unambiguous one-byte
|
||||
# truncation; nested truncation, duplicate keys and schema errors remain
|
||||
# invalid. The response exposes the repair so the caller can report it.
|
||||
candidate = proposal_json.rstrip()
|
||||
if error.pos != len(candidate) or not candidate.startswith("{"):
|
||||
raise ValueError("Proposal does not match the GEWU v1 contract") from error
|
||||
repaired_json = candidate + "}"
|
||||
try:
|
||||
parsed = json.loads(repaired_json, object_pairs_hook=_reject_duplicate_keys)
|
||||
proposal = ProposalRevision.model_validate_json(repaired_json, strict=True)
|
||||
except (ValueError, ValidationError) as repaired_error:
|
||||
raise ValueError("Proposal does not match the GEWU v1 contract") from repaired_error
|
||||
return proposal, True
|
||||
except (ValueError, ValidationError) as error:
|
||||
raise ValueError("Proposal does not match the GEWU v1 contract") from error
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError("Proposal must be a JSON object")
|
||||
return proposal
|
||||
return proposal, False
|
||||
|
||||
|
||||
def _parse_run_request[RequestT: (RunCreateRequest, RunCheckpointRequest)](
|
||||
|
||||
@@ -252,6 +252,38 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
assert prepared_proposal.payload_digest == proposal_payload_digest(
|
||||
prepared_proposal
|
||||
)
|
||||
truncated_evaluation = await mcp.call_tool(
|
||||
"proposal_evaluate",
|
||||
{"proposal_json": prepared_payload["proposal_json"][:-1]},
|
||||
)
|
||||
assert not truncated_evaluation.is_error, truncated_evaluation.content
|
||||
truncated_evaluation_payload = json.loads(
|
||||
_text_payload(truncated_evaluation)
|
||||
)
|
||||
assert truncated_evaluation_payload["proposal_id"] == (
|
||||
prepared_proposal.proposal_id
|
||||
)
|
||||
assert truncated_evaluation_payload["input_repaired"] is True
|
||||
malformed_prepare = await mcp.call_tool(
|
||||
"proposal_prepare", {"proposal_json": '{"schema_version":'}
|
||||
)
|
||||
assert not malformed_prepare.is_error, malformed_prepare.content
|
||||
malformed_prepare_payload = json.loads(_text_payload(malformed_prepare))
|
||||
assert malformed_prepare_payload["status"] == "invalid"
|
||||
assert malformed_prepare_payload["issues"][0]["code"] == (
|
||||
"invalid_proposal_draft"
|
||||
)
|
||||
malformed_evaluation = await mcp.call_tool(
|
||||
"proposal_evaluate", {"proposal_json": '{"schema_version":'}
|
||||
)
|
||||
assert not malformed_evaluation.is_error, malformed_evaluation.content
|
||||
malformed_evaluation_payload = json.loads(
|
||||
_text_payload(malformed_evaluation)
|
||||
)
|
||||
assert malformed_evaluation_payload["status"] == "invalid"
|
||||
assert malformed_evaluation_payload["findings"][0]["code"] == (
|
||||
"PROPOSAL_JSON_INVALID"
|
||||
)
|
||||
invalid_draft = json.loads(json.dumps(draft))
|
||||
invalid_evidence = invalid_draft["operations"][1]["node"]["claims"][0][
|
||||
"evidence"
|
||||
@@ -928,12 +960,13 @@ def test_mcp_source_to_citation_vertical_slice(tmp_path: Path) -> None:
|
||||
|
||||
submitted = await mcp.call_tool(
|
||||
"proposal_submit",
|
||||
{"proposal_json": candidate.model_dump_json()},
|
||||
{"proposal_json": candidate.model_dump_json()[:-1]},
|
||||
)
|
||||
assert not submitted.is_error, submitted.content
|
||||
outcome = json.loads(_text_payload(submitted))
|
||||
assert outcome["status"] == "accepted"
|
||||
assert outcome["validation_status"] == "eligible"
|
||||
assert outcome["input_repaired"] is True
|
||||
assert outcome["receipt"] is not None
|
||||
accepted_head = repository.head()
|
||||
retried = await mcp.call_tool(
|
||||
|
||||
Reference in new issue
Block a user