Skip to content

feat(api): cancel accepted project-histories on loopback HTTP - #447

Closed
seonghobae wants to merge 1 commit into
feat/project-history-retrieval-get-gap-003afrom
feat/project-history-cancel-http-gap-003a
Closed

feat(api): cancel accepted project-histories on loopback HTTP#447
seonghobae wants to merge 1 commit into
feat/project-history-retrieval-get-gap-003afrom
feat/project-history-cancel-http-gap-003a

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

GAP-003A unique operator-visible slice: loopback POST /v1/project-histories/{idempotency_key}/cancel on AnalysisRunLiveService / tepp-loopback removes one accepted LineageWeave project-history identity.

  • Empty body admitted; leftover nonempty body fail closed.
  • Public bind / unpublished consumer / present idempotency-key / extra path segments / slash/NUL identities / credential headers fail closed.
  • Metric-free receipts with cancelled=true and inference_status=temporal_association_only. Evidence text, findings, RMSE, bias, coverage, SE-gate, causal scores, and tepp.scientific_acceptance.v1 never appear.
  • Cancelled identities drop from collection GET and GET-by-id.
  • Stacked on feat(api): retrieve project history via loopback GET-by-id #429 (feat/project-history-retrieval-get-gap-003a). Does not unstack onto main.
  • ADR 0079 (0026–0078 occupied). Does not reuse 0026–0078.

Explicitly not

Another export cancel CLI (#446), export cancel HTTP (#445), export collection CLI (#444), export collection GET (#443), interpretation-run cancel CLI (#442), interpretation-run cancel HTTP (#440), project-history retrieval CLI (#431), project-history GET-by-id (#429), project-history collection CLI (#428), project-history collection GET (#424), project-history POST CLI (#420), analysis-run cancel (#361), Leiden, Driver p.16, GAP-010 Figma/export, persistence/Compose (#287).

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

Test plan

  • cargo test -p tepp_api --lib project_history_cancel
  • cargo test -p tepp_api --lib project_history_cancel_removes
  • cargo test -p tepp_api --test project_history_cancel_http_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

POST /v1/project-histories/{idempotency_key}/cancel removes one metric-free
LineageWeave identity from AnalysisRunLiveService. Receipts stay
cancelled=true with temporal_association_only. Naruon is refused.
NaruonLiveService stays POST-only. ADR 0079.
@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: 96ae051e-7783-47ce-afef-877948ebae1e

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 7 potential issues.

Devin Review

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 PR provides no 100% line and branch coverage result for the new route, receipt validation, path decoder, and exchange builder.

Devin Review

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

Comment on lines +340 to +341
if !body.trim().is_empty() {
return Err(ApiError::InvalidWirePayload);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Whitespace bodies cancel histories

A whitespace-only body passes trim().is_empty() and cancels the history. The contract requires every nonempty body to fail closed.

Suggested change
if !body.trim().is_empty() {
return Err(ApiError::InvalidWirePayload);
if !body.is_empty() {
return Err(ApiError::InvalidWirePayload);
}
Devin Review

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

Comment on lines +359 to +365
let (request, projection) = self
.accepted_project_histories
.remove(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
let cancelled = ProjectHistoryCancelled::from_stored(&request, &projection)?;
let response_body = cancelled.to_json()?;
Ok(json_response(200, "OK", response_body))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Failed receipts still delete histories

A project key containing tepp.scientific_acceptance.v1 makes to_json() fail after removal. The caller receives an error, but the history is gone.

Suggested change
let (request, projection) = self
.accepted_project_histories
.remove(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
let cancelled = ProjectHistoryCancelled::from_stored(&request, &projection)?;
let response_body = cancelled.to_json()?;
Ok(json_response(200, "OK", response_body))
let (request, projection) = self
.accepted_project_histories
.get(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;
let cancelled = ProjectHistoryCancelled::from_stored(request, projection)?;
let response_body = cancelled.to_json()?;
self.accepted_project_histories.remove(&replay_key);
Ok(json_response(200, "OK", response_body))
Devin Review

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

Comment on lines +35 to +45
pub struct ProjectHistoryCancelled {
/// Consumer-owned stable project key.
pub project_key: String,
/// Exact request idempotency key that minted the stored projection.
pub idempotency_key: String,
/// Knowledge cutoff applied to the stored projection.
pub knowledge_cutoff: String,
/// Fixed claim boundary: sequence is association, not causation.
pub inference_status: String,
/// Always `true` on a successful cancel receipt.
pub cancelled: bool,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Cancellation receipts lack version identity

ProjectHistoryCancelled omits a semantic contract version. Consumers cannot identify or safely evolve this externally consumable response shape.

Prompt for agents
Add an explicit semantic contract version to the project-history cancellation receipt in crates/tepp_api/src/project_history_cancel_http.rs. Define and export a payload-family version constant, populate it in ProjectHistoryCancelled::from_stored, validate it during serialization and parsing, and update tests and contract documentation. This is required by docs/API_CONTRACT.md section 3 for every externally consumable contract.
Devin Review

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

Comment on lines +169 to +173
if matches!(
project_history_cancel_path_id(path),
Ok(_) | Err(ApiError::LimitExceeded)
) {
return self.cancel_project_history(path, &headers, body);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Oversized paths retain status semantics

Dispatch forwards LimitExceeded paths into the cancel handler, preserving HTTP 413. Other malformed cancel paths cannot reach an existing POST endpoint.

Devin Review

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

Comment on lines +354 to +358
let tenant_workspace_id = header_value(headers, PROJECT_HISTORY_RETRIEVAL_TENANT_HEADER)?;
crate::project_history::validate_project_history_registry_identity(tenant_workspace_id)?;
let idempotency_key = project_history_cancel_path_id(path)?;
let replay_key =
consumer_tenant_idempotency_key(consumer, tenant_workspace_id, &idempotency_key);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Tenant scoping isolates cancellation

The removal key combines consumer, tenant, and decoded identifier. Matching identifiers in different tenants address distinct stored histories.

Devin Review

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

Comment on lines +354 to +362
let tenant_workspace_id = header_value(headers, PROJECT_HISTORY_RETRIEVAL_TENANT_HEADER)?;
crate::project_history::validate_project_history_registry_identity(tenant_workspace_id)?;
let idempotency_key = project_history_cancel_path_id(path)?;
let replay_key =
consumer_tenant_idempotency_key(consumer, tenant_workspace_id, &idempotency_key);
let (request, projection) = self
.accepted_project_histories
.remove(&replay_key)
.ok_or(ApiError::InvalidWirePayload)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟥 Tenant header permits unauthorized cancellation

Any local process can set tepp-tenant-workspace-id and tepp-consumer, then cancel a matching history. The endpoint verifies identity strings but no authorization grant.

Devin Review

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

Copy link
Copy Markdown
Contributor Author

Closing as unsafe destructive-operation lineage with replacement mapping to #174. Review proved forged tenant/consumer headers can cancel another stored history, whitespace is incorrectly admitted as an empty body, receipt failure can delete state, and the external receipt lacks semantic version identity. Preserve those RED findings and project-history cancellation tests for the future authenticated Analysis Run/project-history operations vehicle.

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