Skip to content

feat(migration): server-attested toolbox translation verdicts - #194

Merged
mikemcdougall merged 6 commits into
trunkfrom
feat/188-server-attested-translation
Aug 10, 2026
Merged

feat(migration): server-attested toolbox translation verdicts#194
mikemcdougall merged 6 commits into
trunkfrom
feat/188-server-attested-translation

Conversation

@mikemcdougall

Copy link
Copy Markdown
Contributor

Closes #188

Summary

The arcpy/toolbox codemod classified every tool from the SDK's own view of the Honua process catalog. That view can drift from the server that would actually run the job, so a migration report could call a tool translated when the submit validator would reject it, or flag one unsupported when the server would accept it — and migration reports are used to decide whether a migration is viable.

honua-server#3040 landed the server side (POST /api/v1/admin/import/toolbox/translation/validate, honua-server#2145). It validates a translated toolbox manifest against the canonical process catalog and returns a per-tool translated / partially-translated / unsupported classification with the specific reasons a tool cannot be fully translated. This PR wires the SDK to it, so a toolbox verdict can be server-attested instead of an SDK-local assertion.

Two rules govern the result:

  • The server wins. Where the two verdicts disagree, the server's is the effective one, the local verdict is retained beside it, and the disagreement is listed rather than silently overwritten. A disagreement is the signal that the SDK's registry has drifted from the catalog.
  • A local verdict is never dressed up as attested. Offline operation still works; the report is stamped local-only with an explicit fallbackReason. Unreachable server, refused credentials, a malformed or incomplete response — all degrade the whole report. There is no partial attestation.

Changes Made

  • honua_admin — typed ToolboxTranslationManifest / ToolboxTranslationReport (+ descriptor, mapping, binding, issue, summary models) and validate_toolbox_translation on both HonuaAdminClient and AsyncHonuaAdminClient. The endpoint is in the admin import group, so it rides the existing admin credential path (api_key / auth_provider) — no new auth mechanism. Sync client regenerated with scripts/gen_sync.py.
  • honua_sdk.migration.attestation (new) — builds the wire manifest from a parsed .pyt / .atbx toolbox (build_pyt_translation_manifest / build_atbx_translation_manifest) and merges a server verdict over the local one (attest_translation). The validator is injected, so the merge logic itself is pure, offline, and carries no dependency on the admin client. Manifests larger than the endpoint's 200-tool cap are submitted in batches and merged, rather than truncated or rejected.
  • honua-migrate--server, --api-key (or $HONUA_ADMIN_API_KEY), --attest-timeout, --attestation, --require-attested on translate, pyt, and atbx. translate now also accepts a .pyt / .atbx toolbox. honua-admin is imported lazily and stays optional: without it the toolbox still translates and the report is simply marked local-only.
  • Binary .tbx refusal made actionable — still never parsed (proprietary container; export-to-open-format is the standing rule, same as .loc/.lox), but the error now carries the concrete ArcGIS Pro export steps that produce a readable .atbx / .pyt, so it reads as a migration instruction rather than a dead end. Shared by both the pyt and modelbuilder readers.
  • resolve_argument_bindings — exposes the source-argument to canonical-parameter pairing that the flattened OGC payload dict had discarded. _translate_call now shares that single resolution instead of duplicating it, and the dead _assign_process_value helper is gone.
  • Docs: docs/honua-gp/codemod-translation-coverage.md gains a "Server-attested verdicts" section; README CLI line updated. compatibility/public-api.json regenerated for the new honua_admin surface.

Scope note

Attestation is toolbox-scoped because the endpoint's manifest declares a toolbox sourceFormat (pyt / atbx / tbx). honua-migrate translate on a bare arcpy .py script therefore refuses --server with an actionable message rather than inventing a format the server would reject; point translate at a .pyt / .atbx toolbox for an attested verdict.

Finding surfaced by this work

The SDK's proposed target parameter names already differ from the canonical catalog for at least geometry.buffer (SDK proposes input_features / distance; the server's shared fixture uses wkb / srid / distance). That drift is exactly what this feature is for — it now shows up as unknown-target-parameter / missing-required-parameter issues and a reported disagreement instead of a confidently wrong local verdict. Reconciling the registry against the live catalog is deliberately not in this PR: the point of the change is that the server is authoritative, and a follow-up should be driven by a real attestation run rather than by guessing.

Explicitly not done

  • No binary .tbx parsing. UnsupportedToolboxError is a policy decision, not an unimplemented stub, and was not "fixed".
  • No local/subprocess execution path and no backend-selection surface (ADR-0063 / tests/test_custom_code_batch_only_policy.py tripwire still passes).

Breaking Changes

None. honua_admin gains new exports and a new method; honua_sdk.migration gains new exports. Existing signatures and report shapes are unchanged, with two additive keys on the toolbox commands' emitted document (attestation, and translationManifest on translate).

Testing

Run locally against Python 3.12 with both packages installed editable (grpc,geopandas extras):

  • ruff check . — All checks passed
  • python scripts/gen_sync.py --check — Generated sync files are up to date; sync/async twins are in lockstep
  • python -m mypy packages/honua-sdk/honua_sdk packages/honua-admin/honua_admin — Success: no issues found in 70 source files
  • python -m pytest tests/ -q --cov=honua_sdk --cov=honua_admin --cov-fail-under=941576 passed, 18 skipped; total coverage 94.36% (gate 94)
  • python -m pytest tests/ -q --cov=honua_sdk --cov-fail-under=93 — 94.09%
  • python -m pytest tests/admin -q --cov=honua_admin --cov-fail-under=93 — 94.71%
  • python scripts/compatibility_gate.py — Compatibility gate passed
  • python scripts/gen_sdk_coverage.py — SDK coverage gate passed

New tests (43 added) cover the four paths the issue names plus the offline default:

  • attested success (including batched submission of a >200-tool toolbox)
  • server-vs-local disagreement in both directions — server verdict wins, disagreement surfaced, local verdict retained
  • server unreachable → local-only with the transport failure as the reason, local verdicts intact
  • unauthorized/failed call → never attested, no serverClassification invented, and --require-attested exits non-zero
  • malformed / wrong-artifact / incomplete server reports, and one failed batch un-attesting the whole toolbox
  • manifest construction: local-path redaction from sourceLabel, unique tool names for multi-step tools, output arguments excluded from parameter mappings, unmapped keywords reported as unsupported constructs
  • honua-admin absent → still translates, marked local-only
  • the binary .tbx refusal carrying its export instructions

The arcpy/toolbox codemod classified every tool from the SDK's own view of
the Honua process catalog. That view can drift from the server that would
actually run the job, so a migration report could call a tool translated
when the submit validator would reject it, or flag one unsupported when the
server would accept it.

honua-server#3040 landed the server side of this
(POST /api/v1/admin/import/toolbox/translation/validate, honua-server#2145):
it validates a translated toolbox manifest against the canonical process
catalog and returns a per-tool translated / partially-translated /
unsupported classification with the reasons a tool cannot be fully
translated. This wires the SDK to it.

- honua_admin: typed manifest/report models plus
  HonuaAdminClient.validate_toolbox_translation (sync + async), on the
  existing admin credential path since the endpoint is in the admin import
  group. No new auth mechanism.
- honua_sdk.migration.attestation: builds the manifest from a parsed .pyt /
  .atbx toolbox and merges a server verdict over the local one. The server
  wins on disagreement and the disagreement is reported rather than
  silently overwritten; a local verdict is never presented as attested.
  Offline, unreachable, unauthorized, and malformed-response paths all
  degrade the whole report to an explicitly marked local-only verdict with
  a stated reason. There is no partial attestation. The validator is
  injected, so the merge logic stays pure and offline.
- honua-migrate: --server / --api-key / --attestation / --require-attested
  on translate, pyt, and atbx. translate now also accepts a .pyt/.atbx
  toolbox; on a bare arcpy .py script it refuses --server rather than
  inventing a toolbox sourceFormat the endpoint would reject.
- Binary .tbx stays a policy refusal, never a parser. The error now carries
  the concrete ArcGIS Pro export steps that produce a readable .atbx/.pyt,
  so it reads as a migration instruction instead of a dead end.
- resolve_argument_bindings exposes the source-argument to canonical-
  parameter pairing the flattened OGC payload had discarded; _translate_call
  now shares that one resolution instead of duplicating it.

Closes #188

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04a3460478

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/honua-sdk/honua_sdk/migration/attestation.py
Comment thread packages/honua-admin/honua_admin/_models.py Outdated
Comment thread packages/honua-sdk/honua_sdk/migration/attestation.py
@mikemcdougall

Copy link
Copy Markdown
Contributor Author

CI note: ephemeral-server-smoke (the honua-gp-eval lane) is red here, and it is pre-existing, not caused by this PR. It has been failing on trunk itself since the 2026-08-05 push, and on every unrelated PR since (dependabot, release-please, the README refresh). The failure is supported-surface pass rate 92% is below required 100% — two honua_gp eval scripts regressing against the pinned live honua-server:nightly-aot image.

This PR touches honua_sdk.migration and honua_admin only; it does not touch packages/honua-gp, which is linted/tested under its own separate gate. Every required check (compatibility, lint, package, test (ubuntu-latest, 3.11/3.12, grpc)) plus the full test matrix, conformance, security-audit, and CodeQL are green.

)

All three let a report claim `attested: true` while the verdict was not, in
fact, fully attested -- the exact failure the issue's acceptance criteria
forbid.

.atbx script tools were missing from the manifest. parse_atbx_toolbox records
them in script_tool_names because their logic lives in an external .py the
reader deliberately does not follow, but the manifest was built from
toolbox.models alone. The server then returned a clean report for a strict
subset of the toolbox and the CLI presented it as whole-toolbox attestation.
Script tools are now submitted with no proposed target -- the honest statement,
since the translator never read the body -- so the report's tool count matches
the toolbox and they come back explicitly unsupported.

The admin response model synthesized artifact identity. from_dict defaulted a
missing artifactKind/artifactVersion to the expected values, and the CLI
serializes the model back to a dict before attest_translation validates it, so
the attestation layer could not tell an error envelope from a genuine v1
report. Both fields are now `str | None`, default None, never filled in
client-side, and to_dict round-trips their absence. _parse_report now REQUIRES
both -- the genuine endpoint always stamps them -- instead of accepting a
missing identity.

Classifications outside the vocabulary were accepted. A value such as
`manual-review` or `translated-v2` became a tool's effective classification
while no summary counter tallied it, so the attested report did not add up.
Classifications are validated against the three declared values and anything
else degrades to local-only.

Adds regression tests for each: an .atbx holding both a model and a script
tool, a 200 missing artifactKind/artifactVersion (both directly and through
the admin model round-trip), and out-of-vocabulary classifications -- each
asserting the result is NOT falsely attested. Also pins the invariant the
vocabulary check protects: an attested summary accounts for every tool.

Related to #188
@mikemcdougall

Copy link
Copy Markdown
Contributor Author

@codex review

All three review findings are fixed in 3da5b61 and each thread has a reply with the specific change and its regression test. Summary:

  • P1 — ATBX script tools missing from the manifest. build_atbx_translation_manifest now submits script_tool_names alongside the models, with no proposed target (the reader never follows the external .py, so no mapping has been established) and a construct pointing at the arcpy .py scanner. The report's toolCount now matches the toolbox.
  • P1 — synthesized report identity. ToolboxTranslationReport.artifact_kind/artifact_version are str | None defaulting to None, from_dict no longer setdefaults them, and to_dict round-trips their absence. _parse_report now requires both rather than accepting a missing identity — that second half was the other side of the same hole.
  • P2 — out-of-vocabulary classifications. Added the frozen CLASSIFICATIONS vocabulary; anything outside it degrades the whole report to local-only with the offending tool and value in fallbackReason.

Please re-check specifically that no path can still reach attested: true while (a) a discovered toolbox tool was never submitted, (b) the response lacked a verifiable v1 identity, or (c) a classification fell outside the three declared values.

Note the branch is level with trunk (0 behind), so no update-branch was needed and prior review context is intact.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3da5b61f20

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/honua-sdk/honua_sdk/migration/attestation.py Outdated
Same false-attestation class as the .atbx script-tool gap, on the other
reader. A .pyt declares its tools as `self.tools = [A, B]`; a name whose
class is not defined in that file -- imported from another module -- stays in
`declared_tool_names` but never materialises into `toolbox.tools`, because
there is no execute() body to classify. The manifest was built from
`toolbox.tools` alone, so the server returned a clean report covering only the
locally-defined tools while the CLI presented it as whole-toolbox attestation.

Declared names absent from the materialised tools are now submitted with no
proposed target -- nothing was read, so nothing is claimed -- and come back
`unsupported`, with a construct pointing at the arcpy .py scanner as the way
to classify them properly.

Both readers now share one `_unresolved_tool_proposals` helper, since .atbx
script tools and imported .pyt tool classes are the same shape: discovered by
name, with no body available.

Tests: a mixed toolbox (one local tool, one imported), the end-to-end
attestation over it asserting the report covers both and its summary adds up,
and the degenerate all-imported toolbox that previously produced an empty
manifest the endpoint would reject outright.

Related to #188
@mikemcdougall

Copy link
Copy Markdown
Contributor Author

@codex review

Fourth finding fixed in 6eb0c01 — the .pyt analogue of the .atbx script-tool gap, which I should have caught when fixing that one. build_pyt_translation_manifest now diffs declared_tool_names against the materialised tools and submits the difference with no proposed target; both readers share one _unresolved_tool_proposals helper so they cannot drift apart again.

The invariant across all four findings, stated once so it is easy to re-check: attested: true requires that every tool the reader discovered — by body or by name alone — was submitted and came back with a classification inside the declared vocabulary, from a response carrying a verifiable v1 artifact identity. Anything short of that degrades the whole report to local-only with a stated reason.

Worth a specific look at whether any other reader surfaces a tool the manifest builder does not submit. I have covered .atbx script_tool_names and .pyt declared_tool_names; if parse_gp_service_definition or the model-step path has an equivalent name-only discovery channel, I would rather hear it now than ship a third instance of this bug.

…#188)

Third instance of the same false-attestation class, found while checking
whether the .pyt and .atbx fixes had missed a sibling. parse_atbx_toolbox
deliberately keeps a stepless model out of `models` -- there is nothing to
translate -- but it also dropped the name entirely, so the tool was invisible
to the manifest builder and the server could return a clean report for a
toolbox that declared more tools than were ever submitted.

ModelBuilderToolbox gains `unresolved_tool_names` (surfaced in to_dict as
`unresolvedToolNames`) holding those declared-but-unresolvable names. The
`models` contract is unchanged -- a stepless model is still not a model -- but
the name is now discoverable rather than lost, and the manifest submits it with
no proposed target.

That makes all three .atbx tool kinds -- models, script tools, and unresolved
models -- go into the manifest, so the report's tool count matches the toolbox.

Tests: the stepless-model case end to end, an .atbx exercising all three kinds
at once, and the pre-existing test_atbx_model_detected_by_tool_type tightened
to pin that the dropped name is now recorded instead of lost.

Related to #188
@mikemcdougall

Copy link
Copy Markdown
Contributor Author

Answering my own question above: there was a third instance, and I found and fixed it in 6a3229f.

parse_atbx_toolbox recognises a model, parses it, and then drops it entirely when it yielded no recognizable geoprocessing step (if model.steps: models.append(model)). The name went nowhere — not into models, not into script_tool_names — so it was invisible to the manifest builder and the server could return a clean report for a toolbox that declares more tools than were ever submitted. The existing test_atbx_model_detected_by_tool_type had pinned that silent drop as intended behaviour.

ModelBuilderToolbox now carries unresolved_tool_names (unresolvedToolNames in to_dict). The models contract is deliberately unchanged — a stepless model is still not a model, and nothing about parity evidence or modelCount moves — but the name is discoverable instead of lost, and the manifest submits it with no proposed target. That existing test is tightened to assert the name is recorded rather than dropped.

All three .atbx tool kinds now reach the manifest: models, script tools, and unresolved models.

I checked the remaining reader surfaces for the same shape and did not find another:

  • parse_gp_service_definition / GpService — not an attestable input at all; the endpoint takes a toolbox manifest (sourceFormat pyt/atbx/tbx), and GP-service definitions go through the separate gpservice command.
  • Model steps_proposals_for_tool already emits a no-target proposal for a tool with zero calls, so an unrecognised step does not vanish.
  • .pytdeclared_tool_names was the only name-only channel, covered by 6eb0c01.

Happy to be proven wrong on that sweep — that is exactly what I would rather hear before this lands than after.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6eb0c01a4d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/honua-sdk/honua_sdk/migration/attestation.py
Comment thread packages/honua-sdk/honua_sdk/migration/attestation.py
… about (#188)

_parse_report checked artifact identity and tool names, but never that the
report was about the artifact just submitted. Two toolboxes can share tool
names, so a stale or misrouted 200 -- a caching proxy, a validator answering
for a different toolbox -- would have had its classifications accepted and the
wrong artifact marked attested.

The endpoint echoes toolboxName and sourceFormat back for exactly this reason.
Both are now compared against the submitted batch before the tools array is
trusted, tolerating the server's own normalisation (it Trim()s the name and
lower-cases the format). A mismatch or a missing field degrades the report to
local-only like any other untrustworthy response.

Tests: wrong/missing toolboxName and wrong/missing sourceFormat each assert
local-only, plus a case proving the server's own trimming and lower-casing
still binds. The shared malformed-report fixtures now carry a valid envelope so
each case still exercises the specific check it names rather than tripping the
new binding check first.

Related to #188
@mikemcdougall

Copy link
Copy Markdown
Contributor Author

Both threads from the latest review are addressed and resolved.

  • Step-less ModelBuilder tools (P1) — already fixed in 6a3229f; that review ran against 6eb0c01, one commit earlier. I had found the same gap independently while sweeping for siblings of the .pyt fix.
  • Bind each report to the submitted toolbox (P2) — fixed in c04eb4b. New to me, and a genuinely different kind of gap: not a parsing slip but a missing trust boundary. _parse_report now checks toolboxName/sourceFormat against the submitted batch before trusting the tools array, tolerating the server's own Trim() / ToLowerInvariant() normalisation.

Running total across this review cycle — six findings, all in the same family, all fixed with regression tests:

# Gap Fix
1 .atbx script tools never submitted 3da5b61
2 Report identity synthesized client-side 3da5b61
3 Classifications outside the vocabulary accepted 3da5b61
4 .pyt tools declared but not materialised never submitted 6eb0c01
5 .atbx models yielding no step never submitted 6a3229f
6 Report not bound to the submitted toolbox c04eb4b

The invariant they all serve, now enforced in one place: attested: true requires that the response carried a verifiable v1 identity, was about the toolbox that was submitted, and classified every tool the reader discovered — by body or by name alone — using only the three declared values. Anything short of that degrades the whole report to local-only with a stated reason. There is no partial attestation.

Local gates on c04eb4b: ruff clean, gen_sync.py --check in lockstep, mypy clean across 70 files, compatibility + SDK-coverage gates pass, 1599 passed / 18 skipped with combined coverage 94.39% (gate 94), honua_sdk 94.08% and honua_admin 94.84% (floors 93).

The only red check is ephemeral-server-smoke, which is not a required context and fails identically on trunk (supported-surface pass rate 92% is below required 100% — same message, same 92%, since the 2026-08-05 trunk run). It exercises packages/honua-gp against a pinned live server image; this PR does not touch that package.

@mikemcdougall
mikemcdougall merged commit d9e8058 into trunk Aug 10, 2026
27 of 28 checks passed
@mikemcdougall
mikemcdougall deleted the feat/188-server-attested-translation branch August 10, 2026 02:10
@github-actions github-actions Bot mentioned this pull request Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(migration): server-attested toolbox translation reports via the validation endpoint

1 participant