Skip to content

feat: generated-surface attachment inputs for send_message (COD-406, IRIS-11 PR3) - #30

Merged
shivros merged 3 commits into
mainfrom
runner/COD-406-surface-attachment-inputs
Aug 20, 2026
Merged

feat: generated-surface attachment inputs for send_message (COD-406, IRIS-11 PR3)#30
shivros merged 3 commits into
mainfrom
runner/COD-406-surface-attachment-inputs

Conversation

@shivros

@shivros shivros commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

COD-406 — IRIS-11 PR3: Generated-surface attachment inputs (T8–T10)

Third implementation slice of the add-outbound-attachments OpenSpec change (7/11 on main after #26). PR1 (#21) landed the core contract + store resolution + provider plumbing; PR2 (#26) landed Telegram multipart + Email MIME sends. This PR closes the gap: the generated surfaces (HTTP/MCP/CLI) can now express attachment inputs.

What changed

T8 — API definition + codegen (via hydra v0.2.0 json-params, adopted from hydra PR#3 / COD-411):

  • api/operations.yaml: send_message gains an attachments body parameter declared as type: json with the closed inline/stored union schema — inline requires mime_type + data_base64, permits only optional filename, forbids stored_id; stored requires only stored_id, forbids inline fields. No name-based inference; the union is declared explicitly.
  • CLI representation overrides: repeatable --attach with companion --attach-mime.
  • crates/iris-codegen pinned to hydra v0.2.0 (c3923dc); generated/cli.rs + generated/mcp.json regenerated.

T9 — wire decoding + CLI planning (new iris-core::wire, pure, no I/O):

  • decode_attachments: union JSON → OutboundAttachment variants, rejecting malformed base64, invalid UUIDs, empty inline bytes, missing MIME, mixed/unknown fields — before any dispatch.
  • plan_attachments: local-path vs iris://attachment/UUID split; --attach-mime cardinality (exactly one per local-path attachment, in order; stored refs consume none) with explicit override of extension MIME inference.
  • HTTP routes + MCP handler decode through the shared module: HTTP 400 / JSON-RPC rejection on malformed unions. CLI reads local files at the boundary and builds the OutboundMessage.

T10 — tests: HTTP 400 on mixed/unknown/bad-base64 unions, MCP tool rejection, CLI flag parsing + boundary reads + mime override ordering, generated-schema union assertions, codegen freshness test (crates/iris-codegen/tests/freshness.rs) mirroring iris-codegen check.

Verification (T11)

All five gates green on this branch, re-verified this run:

  • cargo build --all-targets
  • cargo test --all-targets ✅ — 180/180 (was 150)
  • cargo clippy --all-targets -- -D warnings
  • cargo fmt --all -- --check
  • cargo run -p iris-codegen --bin iris-codegen -- check

(imap-proto future-incompat warning is pre-existing on main.)

OpenSpec add-outbound-attachments tasks T8–T11 checked on this branch; the change is now 11/11 pending this merge.

Linear: COD-406

Adopt hydra v0.2.0 json parameters: declare `attachments` in
api/operations.yaml as a body json parameter carrying the closed
inline/stored union schema plus CLI representation overrides
(repeatable --attach with companion --attach-mime). Regenerate
CLI/MCP artifacts and pin hydra to tag v0.2.0.

Runtime decoding lives in iris-core::wire (pure, no I/O):
- decode_attachments: union -> OutboundAttachment::Bytes/Stored,
  rejecting malformed base64, invalid UUIDs, empty inline bytes,
  missing MIME, mixed/unknown fields before any dispatch
- plan_attachments: local-path vs iris://attachment/UUID split,
  --attach-mime cardinality (exactly one per local path, in order,
  stored refs consume none) with explicit override of extension
  inference

HTTP routes and the MCP handler decode through the shared module
(400 / JSON-RPC rejection on malformed unions); the CLI reads local
files at the boundary and builds the OutboundMessage.

Surface tests: HTTP 400 on mixed/unknown/bad-base64, MCP tool
rejection, CLI flag parsing + boundary reads + mime override
ordering, generated-schema union assertions, and a codegen
freshness test mirroring `iris-codegen check`.

180/180 tests; build/clippy/fmt/codegen green.
…anel)

Review panel findings on COD-406:
- Reviewer B [MAJOR]: decode_item mapped explicit `filename: null` to None,
  widening the declared optional-but-not-nullable union schema. Now rejected
  before dispatch like any other unknown/malformed union member.
- Reviewer A [MAJOR]: CLI --attach-mime overrides accepted empty/whitespace
  values while HTTP/MCP reject blank mime_type. plan_attachments now rejects
  blank overrides, keeping all three surfaces on the same contract.

Both regression-tested in iris-core::wire.

The panel's Docker-deletion BLOCK was a false positive caused by base drift:
PR #29 added Docker files to main after this branch was cut; the rebase onto
0c6293c resolves it. No Docker files are touched by this branch.
@shivros

shivros commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Automated Review Panel

Panel: dual independent review of the branch diff (pre-PR). Findings triaged below; blocking/major items were fixed in c20a938 before this PR was opened.

Note on the shared BLOCK finding: both reviewers flagged "deletes Dockerfile/.dockerignore/docker-entrypoint.sh." That was a false positive from base drift — PR #29 added those Docker files to main after this branch was cut, and the review diff was generated against the newly-fetched main before rebase. This branch never touched any Docker files; the rebase onto 0c6293c resolves it. Verified via git ls-tree at the original branch base (no Docker files present).


Reviewer A (glm-5.1, panel seat A)

BLOCK: unrelated Docker packaging files are deleted, causing a deployment regression outside COD-406 scope.

  1. [BLOCK] Dockerfile:1, .dockerignore:1, docker-entrypoint.sh:1 — This PR deletes the Docker build/runtime assets entirely. (→ resolved: false positive from base drift, see note above.)
  2. [MAJOR] crates/iris-core/src/wire.rs / crates/iris-cli/src/commands.rs — CLI --attach-mime overrides are accepted without validating that the MIME value is non-empty/non-whitespace. HTTP/MCP inline attachments reject empty mime_type in decode_item, but CLI local-path attachments bypass that decoder and can construct OutboundAttachment::Bytes { mime_type: "" | " ", ... }. This leaves the generated CLI surface less strict than HTTP/MCP for the same public contract. → Fixed in c20a938 (plan_attachments rejects blank overrides) + regression test cli_blank_attach_mime_override_is_rejected.
  3. [NOTE] The core T8/T9 shape is otherwise aligned: the YAML/MCP schema declares the closed inline/stored union, HTTP and MCP both decode through shared iris_core::decode_attachments, and malformed base64, invalid UUIDs, empty inline bytes, mixed fields, and unknown union fields are rejected before provider dispatch.
  4. [NOTE] CLI attachment planning mostly matches the stated contract: --attach supports local paths and iris://attachment/UUID, stored refs consume no --attach-mime, and cardinality is enforced exactly against local-path attachments.
  5. [NOTE] T10 coverage is present for real HTTP/MCP rejection paths, generated MCP schema shape, CLI parsing/planning, and codegen freshness.

Reviewer B (glm-5.1, panel seat B)

BLOCK: unrelated deletion of Docker packaging files regresses self-hostable/container deployment and is outside COD-406 scope.

  1. [BLOCK] Unrelated container packaging was removed; no replacement Docker files in the repo. (→ resolved: false positive from base drift, see note above.)
  2. [MAJOR] HTTP/MCP runtime accepts filename: null, which is outside the declared closed union schema. api/operations.yaml declares inline filename as type: string and optional, not nullable, but decode_item maps Some(Value::Null) to None. That widens the public wire contract beyond the generated schema and the "closed inline/stored union" goal. → Fixed in c20a938 (explicit null now rejected) + regression test null_filename_is_rejected.
  3. [MINOR] CLI --attach-mime values are not validated as non-empty. → Fixed in c20a938 (same change as Reviewer A feat: add provider configuration registry #2).
  4. [NOTE] The main COD-406 attachment path otherwise appears aligned: api/operations.yaml declares an explicit oneOf closed inline/stored union; HTTP and MCP both decode through shared iris_core::decode_attachments before provider dispatch; malformed base64, invalid UUIDs, empty inline bytes, mixed variants, and unknown fields are rejected; CLI planning handles local paths vs stored refs and --attach-mime cardinality/order; tests cover HTTP 400 paths, MCP rejection, CLI parsing/planning, and codegen freshness.

Post-fix verification: 182/182 tests (+2 regression), build/clippy -D warnings/fmt/codegen-check all green on c20a938.

@shivros
shivros marked this pull request as ready for review August 20, 2026 03:53
@shivros

shivros commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Auto-Merge Gate rationale

Confidence: 0.93

COD-406's stated T8–T11 goal is clearly implemented: an explicit closed inline/stored attachment union is declared in api/operations.yaml, regenerated for CLI/MCP, and decoded through a shared pure core layer before HTTP/MCP/provider dispatch. CLI local paths, stored refs, MIME cardinality/order, invalid input paths, and codegen freshness all have focused coverage.

Checks observed: GitHub CI Test, Format Check, Clippy, Secret Scan, Dependency Audit, and Creed context drift all passed. The gate also re-ran cargo test --all-targets, cargo clippy --all-targets -- -D warnings, cargo fmt --all -- --check, and cargo run -p iris-codegen --bin iris-codegen -- check successfully at the PR head.

Scope/safety: 15 files, confined to the attachment API/codegen/runtime surfaces and their tests. No secrets, deployment/cutover, billing, destructive behavior, or manual-review marker was found in COD-406 or the PR. The prior review-panel concerns were addressed in the final head commit and have regression tests.

@shivros
shivros merged commit 0ea1f8c into main Aug 20, 2026
6 checks passed
@shivros
shivros deleted the runner/COD-406-surface-attachment-inputs branch August 20, 2026 03:53
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.

1 participant