Skip to content

feat(api): cancel authorized exports via loopback CLI - #446

Closed
seonghobae wants to merge 1 commit into
feat/export-cancel-http-gap-003afrom
feat/export-cancel-cli-gap-003a
Closed

feat(api): cancel authorized exports via loopback CLI#446
seonghobae wants to merge 1 commit into
feat/export-cancel-http-gap-003afrom
feat/export-cancel-cli-gap-003a

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

GAP-003A unique operator-visible slice: published tepp-export-cancel cancel mints typed naruon_export_cancel_exchange onto spawned tepp-loopback TCP.

Explicitly not

Another export cancel HTTP (#445), export collection CLI (#444), export collection GET (#443), interpretation-run cancel CLI (#442), interpretation-run cancel HTTP (#440), interpretation-run collection CLI (#436), export-retrieval CLI (#417), export retrieval GET (#411), export-authorize CLI (#410), analysis-run cancel (#361), Leiden, Driver p.16, GAP-010 Figma/export, persistence/Compose (#287).

Does not add GET to NaruonLiveService. Does not open LineageWeave on this naruon-owned adapter. Does not weaken fail-closed. Does not infer causality. HTTP 200 is not an ADR 0014 claim.

Test plan

  • cargo test -p tepp_api --lib export_cancel_cli
  • cargo test -p tepp_api --test export_cancel_cli_contract
  • cargo clippy -p tepp_api --all-targets -- -D warnings
  • cargo doc -p tepp_api --no-deps
  • python3 scripts/validate_documentation.py
  • python3 scripts/check_docstrings.py

Devin Review

Publish tepp-export-cancel cancel so operators mint naruon POST
/v1/exports/{export_id}/cancel onto spawned tepp-loopback TCP. Receipts stay
metric-free cancelled=true. LineageWeave is refused. NaruonLiveService stays
POST-only. ADR 0078.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 869b7265-58ad-4b94-9c9a-f9e3e92cbff4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@devin-ai-integration devin-ai-integration 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.

Devin Review found 3 potential issues.

Devin Review

Comment on lines +331 to +335
let parsed = ExportCancelled::from_json(&response.body)?;
if !parsed.cancelled {
return Err(ApiError::InvalidWirePayload);
}
parsed.to_json()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Wrong-export cancellation reports success

When a valid receipt names another export, render_export_cancel_cli_stdout prints it as success. The requested export can remain active while operators see cancellation.

Suggested change
let parsed = ExportCancelled::from_json(&response.body)?;
if !parsed.cancelled {
return Err(ApiError::InvalidWirePayload);
}
parsed.to_json()
let parsed = ExportCancelled::from_json(&response.body)?;
if !parsed.cancelled || parsed.export_id != invocation.export_id {
return Err(ApiError::InvalidWirePayload);
}
parsed.to_json()
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +304 to +307
let mut bytes = Vec::new();
stream
.read_to_end(&mut bytes)
.map_err(|error| map_io_error(&error))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Unbounded CLI input exhausts memory

A continuously writing peer makes read_to_end grow memory without limit. Piped stdin has the same flaw, so either stream can terminate the CLI.

Prompt for agents
Bound both untrusted input paths in crates/tepp_api/src/export_cancel_cli.rs. execute_export_cancel_cli currently reads the entire TCP response with read_to_end, and read_export_cancel_cli_stdin reads all piped stdin with read_to_string. Introduce a bounded reader that consumes at most the applicable wire limit plus one byte, returns ApiError::LimitExceeded on overflow, and preserves UTF-8 validation for stdin. Bound the HTTP response by the maximum allowed response header plus ExportCancelled/error-envelope body size before parsing. Add tests with over-limit streams and a reader that keeps producing data.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +346 to +395
fn parse_http_response(bytes: &[u8]) -> Result<NaruonLiveResponse, ApiError> {
let text = std::str::from_utf8(bytes).map_err(|_| ApiError::InvalidWirePayload)?;
let (header_block, body) = text
.split_once("\r\n\r\n")
.ok_or(ApiError::InvalidWirePayload)?;
let mut lines = header_block.split("\r\n");
let status_line = lines.next().ok_or(ApiError::InvalidWirePayload)?;
let mut parts = status_line.split(' ');
if parts.next() != Some("HTTP/1.1") {
return Err(ApiError::InvalidWirePayload);
}
let code = parts
.next()
.ok_or(ApiError::InvalidWirePayload)?
.parse::<u16>()
.map_err(|_| ApiError::InvalidWirePayload)?;
let reason_phrase = match code {
200 => "OK",
202 => "Accepted",
400 => "Bad Request",
403 => "Forbidden",
413 => "Payload Too Large",
422 => "Unprocessable Entity",
_ => return Err(ApiError::InvalidWirePayload),
};
let mut content_length = None;
for line in lines {
let (name, value) = line.split_once(':').ok_or(ApiError::InvalidWirePayload)?;
if name.eq_ignore_ascii_case("content-length") {
if content_length.is_some() {
return Err(ApiError::InvalidWirePayload);
}
content_length = Some(
value
.trim()
.parse::<usize>()
.map_err(|_| ApiError::InvalidWirePayload)?,
);
}
}
let declared = content_length.ok_or(ApiError::InvalidWirePayload)?;
if declared != body.len() {
return Err(ApiError::InvalidWirePayload);
}
Ok(NaruonLiveResponse {
status_code: code,
reason_phrase,
body: body.to_owned(),
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 Required coverage evidence is absent

The verification list omits the mandatory 100% production line and branch coverage gate. The new parser and binary contain untested failure branches.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Closing with replacement mapping to #174. This CLI depends on #445's unauthenticated export mutation and cannot repair that trust boundary. Preserve its operator parsing/framing/stdout/refusal tests for the future authenticated export-operations CLI after principal/resource/purpose authorization exists.

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