Skip to content

feat: add approval request intake and bypass-request promotion - #5074

Open
daviddanialy wants to merge 4 commits into
daviddanialy/ais-468-feat-org-exposure-signalsfrom
daviddanialy/ais-469-feat-approval-request-intake
Open

feat: add approval request intake and bypass-request promotion#5074
daviddanialy wants to merge 4 commits into
daviddanialy/ais-468-feat-org-exposure-signalsfrom
daviddanialy/ais-469-feat-approval-request-intake

Conversation

@daviddanialy

@daviddanialy daviddanialy commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Adds approval-request intake (AIS-469) — the two entry points that need no surface-placement decision. Stacked on the org-exposure-signals branch; review that stack bottom-up.

What it does

mcpApproval.createRequest — the proactive path. A member names a server (URL or stdio launch command) and says why. Deliberately carries no RBAC scope, per the ticket's decision: the people asking typically cannot reach the dashboard, and a scope for it would either be ungranted for everyone who needs it or granted so universally it means nothing — the same posture as the block and bypass surfaces. Authentication, project membership, and the product-feature gate still apply, each tested.

mcpApproval.promotedecide-scoped. Brings a risk-policy bypass request (minted by a shadow-MCP block) into the review queue, carrying the blocked employee as requester with their justification, and linking the source on risk_policy_bypass_request_id. Whole-policy bypasses are rejected (they name no server), and a bypass whose hook couldn't resolve a user still promotes — the ask isn't lost, it just carries no requester attribution.

The promote path is the sharpest IDOR surface in this workflow

The bypass-request id is caller-supplied, and there is deliberately no database-level pin between bypass requests and projects for this pair (adding one would have made the schema migration non-transactional). So the handler's project-scoped resolve is the primary control: the id is read under the caller's project before anything is written, and the cross-project test asserts both the 404 and that nothing entered the queue.

Dedupe converges across entry points

Server URLs canonicalize with the same function that keys the shadow-MCP inventory, so a proactive ask, a promoted block, and the org's own observed traffic land on one review per serverhttps://MCP.Example.com:443/sse#x and https://mcp.example.com/sse are the same request, whichever door it came through. One row per person per request: a repeat ask keeps the freshest justification without erasing an earlier one when the new ask carries none. A re-request reopens a denied review with its decision history intact.

Also

  • Identity resolution runs at intake, so artifact_ref / version_pinned are populated before an admin looks; an unidentifiable reference is stored as explicitly unresolved, never as empty.
  • Every admission audits as mcp_approval_request:create in the same transaction.

Evidence assembly at intake

Every admission now gathers the deterministic evidence into the versioned current_evidence document — resolved identity (always present, unknown shown as unknown), registry metadata for package references (with package_not_published as a distinct clean outcome), and the org's own traffic exposure for remote ones (fed the resolved URL, so mcp-remote stdio commands hit the inventory). Per-source failures land in a gaps list inside the document rather than reading as checked-and-clean, gathering runs before the transaction under a bounded timeout, and a flaky registry can delay evidence but never lose an admission.

The optional research agent deliberately stays outside this document: web-sourced findings keep their own trust tier and lifecycle in mcp_research_reports, and this document doubles as the agent's briefing when an admin later triggers a run — DecodeDocument is the version-dispatched typed read the runner (and any frozen-snapshot reader) will use.

Not in this PR

Where the employee-facing proactive surface lives (block-page family vs device-agent) — the ticket defers that to the design partner. End-to-end verification of the non-admin block-page auth path is also outstanding.

Testing

13 new tests (44 total in the package): both happy paths, cross-project promote, dedupe across users and entry points, repeat-ask note semantics, unattributed requester, feature gate, scope boundary, audit delta. mise run lint:server, mise run gen:sdk, and pnpm -F dashboard type-check all pass.

AIS-469

🤖 Generated with Claude Code


Summary by cubic

Adds employee approval-request intake and admin bypass-request promotion for MCP approvals. Also assembles a versioned evidence document at intake so admins have identity, registry, and traffic signals ready (AIS-469).

  • New Features

    • mcpApproval.createRequest: members ask by server URL or stdio command; no RBAC scope; auth, project membership, and feature gate enforced.
    • mcpApproval.promote (decide-scoped): promotes a bypass into the queue, links risk_policy_bypass_request_id, validates the id within the caller’s project, rejects whole-policy bypasses, supports unattributed users.
    • Dedupe across entry points via canonical server keys; repeat asks attach; re-requests reopen denied reviews without losing history.
    • Evidence at intake: builds versioned current_evidence with identity, package-registry metadata, and org traffic exposure; records gaps; bounded timeout with per-source deadlines; pooled registry client to reduce churn; research agent stays separate.
    • Auditing + SDK/CLI: every intake logs mcp_approval_request:create; OpenAPI updated; new client methods and React Query hooks (createMcpApprovalRequest, promoteMcpApprovalRequest); CLI adds create-request and promote.
  • Bug Fixes

    • Redacts server URLs (scheme/host/path) before persisting/auditing; only http/https targets allowed; target_kind enforced as an enum at the API boundary.
    • createRequest requires a non-blank note; promote note stays optional and won’t erase an existing justification; reopen guards verified; audit event wording now covers creates.

Written for commit faf4073. Summary will update on new commits.

Review in cubic

@daviddanialy
daviddanialy requested review from a team as code owners August 7, 2026 20:26
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

AIS-469

@daviddanialy daviddanialy added the enhancement New feature or request label Aug 7, 2026
@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: faf4073

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
server Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@daviddanialy
daviddanialy force-pushed the daviddanialy/ais-468-feat-org-exposure-signals branch from 27e91de to 5f6ccfd Compare August 7, 2026 20:36
@daviddanialy
daviddanialy force-pushed the daviddanialy/ais-469-feat-approval-request-intake branch from a501aad to be1c01e Compare August 7, 2026 20:38

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cubic analysis

All reported issues were addressed

Linked issue analysis

Linked issue: AIS-469: feat: approval request intake for employees without dashboard access

Status Acceptance criteria Notes
Proactive intake endpoint to let a non-admin member request review (mcpApproval.createRequest) that accepts a target and a free-text justification (note) and intentionally carries no new RBAC scope Handler, API surface, SDK and CLI were added for createRequest; payload includes target, targetKind, and note; tests exercise createRequest and scope/feature-gate boundaries.
Promote a shadow-MCP bypass request into the approval queue and carry the blocked employee as the requester with their justification (mcpApproval.promote), rejecting whole-policy bypasses and allowing unattributed requests Promote endpoint and plumbing implemented; promote links risk_policy_bypass_request_id and preserves requester/note handling; tests include cross-project safety and unattributed-requester behavior.
Dedupe: proactive asks, promoted blocks, and observed traffic converge on one review per canonicalized server reference Target canonicalization used when creating/upserting requests so repeated asks attach to the same review; DB upsert preserves existing rows and re-opens denied reviews per description and tests.
Identity resolution runs at intake so evidence fields (artifact_ref / version_pinned) are populated or explicitly marked unresolved before an admin looks Intake invokes identity resolution and stores resolved references or explicit unresolved markers; tests exercise identity/resolve semantics and repeat-ask note behavior.
Every intake is audited as mcp_approval_request:create in the same transaction Audit action and logger added; tests include audit delta assertions.

Tip: instead of fixing issues one by one fix them all with cubic

Re-trigger cubic

Comment thread server/design/mcpapproval/design.go Outdated
Comment thread server/internal/audit/mcpapprovalrequests.go
Comment thread server/internal/mcpapproval/impl.go Outdated
Comment thread server/internal/mcpapproval/impl.go
Comment thread server/internal/mcpapproval/create_request_test.go
Comment thread server/design/mcpapproval/design.go Outdated
@daviddanialy
daviddanialy force-pushed the daviddanialy/ais-468-feat-org-exposure-signals branch from 5f6ccfd to bd6eee1 Compare August 7, 2026 20:52
@daviddanialy
daviddanialy force-pushed the daviddanialy/ais-469-feat-approval-request-intake branch from 5a7eac3 to 0c8e5be Compare August 7, 2026 20:52

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread server/cmd/gram/start.go Outdated
Comment thread server/internal/mcpapproval/evidence/evidence.go
Comment thread server/internal/mcpapproval/impl.go
daviddanialy and others added 3 commits August 7, 2026 14:15
The two entry points from AIS-469 that need no placement decision: a
proactive createRequest for members, and promote for admins bringing a
risk-policy bypass request into the review queue.

createRequest deliberately carries no RBAC scope — the people asking
typically cannot reach the dashboard, and a scope for it would either
be ungranted for everyone who needs it or granted so universally it
means nothing, the same posture as the block and bypass surfaces.
Authentication, project membership, and the product-feature gate still
apply.

promote resolves the caller-supplied bypass-request id under the
caller's project before anything is written. There is deliberately no
database-level pin for this pair, so that read is the primary control
against promoting another project's bypass request into this project's
queue — the sharpest IDOR surface in the workflow, and the reason the
cross-project case has its own test asserting nothing is written.

Server URLs canonicalize with the same function that keys the
shadow-MCP inventory, so a proactive ask, a promoted block, and the
org's own traffic converge on one review per server. One row per person
per request: a repeat ask keeps the freshest justification without
erasing an earlier one when the new ask carries none. Identity
resolution runs at intake, and every admission audits as a create
against the request subject.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Server URLs are persisted and audited in redacted form — scheme, host
  and path only — reusing the identity package's redaction. A token
  pasted into a request URL must reach neither the review queue nor the
  audit feed; the test that previously demonstrated the leak now pins
  the redaction.
- Only http and https references are admitted, on both intake paths:
  the MCP backend can reach nothing else, and a review for an
  unreachable reference wastes an admin's attention.
- A proactive ask requires a non-blank justification at the payload
  boundary, per the ticket; a promoted bypass keeps its note optional
  since one cannot be conjured, and an empty bypass note no longer
  erases a justification the same person gave proactively — now tested.
- target_kind is an enum in the published contract, so invalid
  namespaces fail at the API boundary.
- The audit outbox event's contract wording covers creates as well as
  decisions, matching the one-event-per-subject convention.
- The reopen guard is now exercised through createRequest for the
  approved-stays and pending-stays cases, with history asserted intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Composes the deterministic signals — resolved identity, registry
metadata for package references, and this org's own traffic for remote
ones — into the versioned current_evidence document, gathered at
admission so evidence is on the request by the time an admin looks.

The shape holds three properties deliberately: found, not-published,
and could-not-look are distinct (a failed lookup lands in the
document's gaps list, never reading as checked-and-clean); everything
gathered is a declaration or an observation of this org's own traffic;
and gathering is best-effort per source under a bounded timeout, so an
admission is never lost to a flaky registry.

The research agent's output deliberately never enters this document —
web-sourced claims keep their own trust tier, lifecycle, and caveats in
mcp_research_reports. This document is instead the agent's briefing:
identity is where its research starts, gaps list what deterministic
gathering could not get, and DecodeDocument gives the future runner and
any frozen-snapshot reader the document typed, version-dispatched so a
version-1 snapshot stays decodable after the shape moves on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@daviddanialy
daviddanialy force-pushed the daviddanialy/ais-468-feat-org-exposure-signals branch from bd6eee1 to 3cff2b2 Compare August 7, 2026 21:15
…ctions

- Each source inside the assembler now carries its own deadline, so an
  unreachable registry costs its own budget and lands in the document's
  gaps rather than holding the admission for the whole gather window.
  The handler's outer timeout remains as a backstop only.
- The package-metadata client uses the pooled guardian client: it is a
  long-lived client making repeated requests to the same two registry
  hosts, which is exactly what PooledClient documents itself for —
  per-request connection churn was the cost of the non-pooled default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant