Repository navigation
Expose inline MCP Apps through curated tool catalogs #8010
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
caf321f
7a3d542
4d03699
71df48c
4b89e09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@agent-native/core": none | ||
| --- | ||
|
|
||
| Carry the shared `isOutsideAgentCall` helper from #8005, whose changeset supplies the release note. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { isOutsideAgentCall } from "./action-caller.js"; | ||
| import type { ActionCaller, ActionRunContext } from "./action.js"; | ||
|
|
||
| const callers = { | ||
| frontend: { outside: false, reason: "the app's own UI" }, | ||
| tool: { outside: false, reason: "the app's own agent without delegation" }, | ||
| http: { | ||
| outside: true, | ||
| reason: | ||
| "HTTP alone does not prove a human UI click; action routes verify UI capability separately", | ||
| }, | ||
| cli: { | ||
| outside: true, | ||
| reason: | ||
| "scripts and the development CLI do not prove an in-app human interaction", | ||
| }, | ||
| automation: { | ||
| outside: true, | ||
| reason: "scheduled or triggered execution is not an interactive human call", | ||
| }, | ||
| mcp: { outside: true, reason: "a direct MCP tool call" }, | ||
| a2a: { outside: true, reason: "a direct A2A action call" }, | ||
| webmcp: { outside: true, reason: "a browser agent's WebMCP call" }, | ||
| "mcp-widget": { outside: true, reason: "a card's scoped read capability" }, | ||
| "mcp-widget-write": { | ||
| outside: true, | ||
| reason: "a card's scoped write capability", | ||
| }, | ||
| } satisfies Record<ActionCaller, { outside: boolean; reason: string }>; | ||
|
|
||
| const protocols = { | ||
| a2a: "the default A2A chat handoff retains tool as the caller", | ||
| mcp: "ask_app runs the app agent as tool on an outside agent's behalf", | ||
| "provider-api": | ||
| "external provider provenance does not establish an in-app human call", | ||
| } satisfies Record<NonNullable<ActionRunContext["networkProtocol"]>, string>; | ||
|
|
||
| describe("isOutsideAgentCall", () => { | ||
| for (const [caller, { outside, reason }] of Object.entries(callers)) { | ||
| const ctx = { caller: caller as ActionCaller }; | ||
|
|
||
| it(`classifies ${caller}: ${reason}`, () => { | ||
| expect(isOutsideAgentCall(ctx)).toBe(outside); | ||
| }); | ||
|
|
||
| it(`treats ${caller} with automation context as outside`, () => { | ||
| expect( | ||
| isOutsideAgentCall({ | ||
| ...ctx, | ||
| automation: { triggerId: "trigger-1", triggerName: "Scheduled run" }, | ||
| }), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| for (const [protocol, reason] of Object.entries(protocols)) { | ||
| it(`treats ${caller} over ${protocol} as outside: ${reason}`, () => { | ||
| expect( | ||
| isOutsideAgentCall({ | ||
| ...ctx, | ||
| networkProtocol: protocol as ActionRunContext["networkProtocol"], | ||
| }), | ||
| ).toBe(true); | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| it("fails closed without context or a recognized caller", () => { | ||
| expect(isOutsideAgentCall()).toBe(true); | ||
| expect(isOutsideAgentCall({} as ActionRunContext)).toBe(true); | ||
| expect( | ||
| isOutsideAgentCall({ caller: "future-caller" } as ActionRunContext), | ||
| ).toBe(true); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import type { ActionRunContext } from "./action.js"; | ||
|
|
||
| /** Classify provenance for approval policy; this does not authenticate a caller. */ | ||
| export function isOutsideAgentCall( | ||
| ctx?: Pick<ActionRunContext, "caller" | "networkProtocol" | "automation">, | ||
| ): boolean { | ||
| // CLI defaults to outside because coding agents drive it; Alice's decision is pending. | ||
| // Automation has no trusted origin in ctx yet; #8070's mcpManaged is job frontmatter. | ||
| return ( | ||
| ctx?.networkProtocol !== undefined || | ||
| ctx?.automation !== undefined || | ||
| (ctx?.caller !== "frontend" && ctx?.caller !== "tool") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 CLI actions bypass the new approval policyThis helper classifies
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the update. The CLI dispatcher still calls handlers directly without evaluating
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for confirming the CLI gap. I’m holding approval until CLI dispatch applies |
||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,275 @@ | ||
| // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||
|
|
||
| exports[`explicit connector inline apps > advertises each inline tool and its UI resource on the default production catalog 1`] = ` | ||
| { | ||
| "analytics": [ | ||
| { | ||
| "_meta": { | ||
| "openai/outputTemplate": "ui://analytics/open-traffic-dashboard/shell-v65", | ||
| "openai/toolInvocation/invoked": "Traffic dashboard ready", | ||
| "openai/toolInvocation/invoking": "Opening Traffic dashboard", | ||
| "openai/widgetAccessible": true, | ||
| "openai/widgetCSP": { | ||
| "connect_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| "frame_domains": [ | ||
| "$requestOrigin", | ||
| ], | ||
| "resource_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| }, | ||
| "ui": { | ||
| "resourceUri": "ui://analytics/open-traffic-dashboard/shell-v65", | ||
| "visibility": [ | ||
| "model", | ||
| "app", | ||
| ], | ||
| }, | ||
| "ui/resourceUri": "ui://analytics/open-traffic-dashboard/shell-v65", | ||
| }, | ||
| "annotations": { | ||
| "destructiveHint": false, | ||
| "openWorldHint": false, | ||
| "readOnlyHint": true, | ||
| "title": "Open traffic dashboard", | ||
| }, | ||
| "name": "open-traffic-dashboard", | ||
| }, | ||
| ], | ||
| "calendar": [ | ||
| { | ||
| "_meta": { | ||
| "openai/outputTemplate": "ui://calendar/manage-event-draft/shell-v65", | ||
| "openai/toolInvocation/invoked": "Review calendar invite ready", | ||
| "openai/toolInvocation/invoking": "Opening Review calendar invite", | ||
| "openai/widgetAccessible": true, | ||
| "openai/widgetCSP": { | ||
| "connect_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| "frame_domains": [ | ||
| "$requestOrigin", | ||
| ], | ||
| "resource_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| }, | ||
| "ui": { | ||
| "resourceUri": "ui://calendar/manage-event-draft/shell-v65", | ||
| "visibility": [ | ||
| "model", | ||
| "app", | ||
| ], | ||
| }, | ||
| "ui/resourceUri": "ui://calendar/manage-event-draft/shell-v65", | ||
| }, | ||
| "annotations": { | ||
| "destructiveHint": true, | ||
| "idempotentHint": false, | ||
| "openWorldHint": false, | ||
| "readOnlyHint": false, | ||
| "title": "Manage event draft", | ||
| }, | ||
| "name": "manage-event-draft", | ||
| }, | ||
| ], | ||
| "content": [ | ||
| { | ||
| "_meta": { | ||
| "openai/outputTemplate": "ui://content/share-local-file-document/shell-v65", | ||
| "openai/toolInvocation/invoked": "Share document ready", | ||
| "openai/toolInvocation/invoking": "Opening Share document", | ||
| "openai/widgetAccessible": true, | ||
| "openai/widgetCSP": { | ||
| "connect_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| "frame_domains": [ | ||
| "$requestOrigin", | ||
| ], | ||
| "resource_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| }, | ||
| "ui": { | ||
| "resourceUri": "ui://content/share-local-file-document/shell-v65", | ||
| "visibility": [ | ||
| "model", | ||
| "app", | ||
| ], | ||
| }, | ||
| "ui/resourceUri": "ui://content/share-local-file-document/shell-v65", | ||
| }, | ||
| "annotations": { | ||
| "destructiveHint": true, | ||
| "idempotentHint": false, | ||
| "openWorldHint": true, | ||
| "readOnlyHint": false, | ||
| "title": "Share local file document", | ||
| }, | ||
| "name": "share-local-file-document", | ||
| }, | ||
| ], | ||
| "design": [ | ||
| { | ||
| "_meta": { | ||
| "openai/outputTemplate": "ui://design/generate-screens/shell-v65", | ||
| "openai/toolInvocation/invoked": "Design generation session ready", | ||
| "openai/toolInvocation/invoking": "Opening Design generation session", | ||
| "openai/widgetAccessible": true, | ||
| "openai/widgetCSP": { | ||
| "connect_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| "frame_domains": [ | ||
| "$requestOrigin", | ||
| ], | ||
| "resource_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| }, | ||
| "ui": { | ||
| "resourceUri": "ui://design/generate-screens/shell-v65", | ||
| "visibility": [ | ||
| "model", | ||
| "app", | ||
| ], | ||
| }, | ||
| "ui/resourceUri": "ui://design/generate-screens/shell-v65", | ||
| }, | ||
| "annotations": { | ||
| "destructiveHint": true, | ||
| "idempotentHint": false, | ||
| "openWorldHint": false, | ||
| "readOnlyHint": false, | ||
| "title": "Generate screens", | ||
| }, | ||
| "name": "generate-screens", | ||
| }, | ||
| { | ||
| "_meta": { | ||
| "openai/outputTemplate": "ui://design/export-coding-handoff/shell-v65", | ||
| "openai/toolInvocation/invoked": "Design agent handoff ready", | ||
| "openai/toolInvocation/invoking": "Opening Design agent handoff", | ||
| "openai/widgetAccessible": true, | ||
| "openai/widgetCSP": { | ||
| "connect_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| "frame_domains": [ | ||
| "$requestOrigin", | ||
| ], | ||
| "resource_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| }, | ||
| "ui": { | ||
| "resourceUri": "ui://design/export-coding-handoff/shell-v65", | ||
| "visibility": [ | ||
| "model", | ||
| "app", | ||
| ], | ||
| }, | ||
| "ui/resourceUri": "ui://design/export-coding-handoff/shell-v65", | ||
| }, | ||
| "annotations": { | ||
| "destructiveHint": false, | ||
| "idempotentHint": false, | ||
| "openWorldHint": true, | ||
| "readOnlyHint": false, | ||
| "title": "Export coding handoff", | ||
| }, | ||
| "name": "export-coding-handoff", | ||
| }, | ||
| ], | ||
| "slides": [ | ||
| { | ||
| "_meta": { | ||
| "openai/outputTemplate": "ui://slides/clone-context-slide/shell-v65", | ||
| "openai/toolInvocation/invoked": "Cloned native slide ready", | ||
| "openai/toolInvocation/invoking": "Opening Cloned native slide", | ||
| "openai/widgetAccessible": true, | ||
| "openai/widgetCSP": { | ||
| "connect_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| "frame_domains": [ | ||
| "$requestOrigin", | ||
| ], | ||
| "resource_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| }, | ||
| "ui": { | ||
| "resourceUri": "ui://slides/clone-context-slide/shell-v65", | ||
| "visibility": [ | ||
| "model", | ||
| "app", | ||
| ], | ||
| }, | ||
| "ui/resourceUri": "ui://slides/clone-context-slide/shell-v65", | ||
| }, | ||
| "annotations": { | ||
| "destructiveHint": false, | ||
| "idempotentHint": false, | ||
| "openWorldHint": false, | ||
| "readOnlyHint": false, | ||
| "title": "Clone context slide", | ||
| }, | ||
| "name": "clone-context-slide", | ||
| }, | ||
| ], | ||
| "slides-2": [ | ||
| { | ||
| "_meta": { | ||
| "openai/outputTemplate": "ui://slides-2/clone-context-slide/shell-v65", | ||
| "openai/toolInvocation/invoked": "Cloned native slide ready", | ||
| "openai/toolInvocation/invoking": "Opening Cloned native slide", | ||
| "openai/widgetAccessible": true, | ||
| "openai/widgetCSP": { | ||
| "connect_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| "frame_domains": [ | ||
| "$requestOrigin", | ||
| ], | ||
| "resource_domains": [ | ||
| "https://esm.sh", | ||
| "$requestOrigin", | ||
| ], | ||
| }, | ||
| "ui": { | ||
| "resourceUri": "ui://slides-2/clone-context-slide/shell-v65", | ||
| "visibility": [ | ||
| "model", | ||
| "app", | ||
| ], | ||
| }, | ||
| "ui/resourceUri": "ui://slides-2/clone-context-slide/shell-v65", | ||
| }, | ||
| "annotations": { | ||
| "destructiveHint": false, | ||
| "openWorldHint": false, | ||
| "readOnlyHint": false, | ||
| "title": "Clone context slide", | ||
| }, | ||
| "name": "clone-context-slide", | ||
| }, | ||
| ], | ||
| } | ||
| `; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Unproven frontend claims bypass approval
The HTTP route can set
caller: "frontend"from the client-controlled frontend header even whenprovenUiRequestis false; it then passes that caller into the approval predicate, which treats it as trusted and skips approval. Only classify frontend calls as trusted after the route verifies the UI capability, and add a regression test for the header without that proof.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update. The route can still treat an arbitrary caller-supplied
x-agent-native-frontend: 1header as trusted even when the UI capability proof fails. Please makefrontendtrusted only after the route verifies that proof and add a regression for the forged header; otherwise outside callers can skip the approval gate.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, this PR waits for #8071. Once #8071 merges, I'll rebase onto main and add a regression at that head: a forged
x-agent-native-frontend: 1request for Calendar'smanage-event-draftdelete-all reachesneedsApprovalashttpand needs approval.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the update. This still depends on #8071 and the follow-up regression, so I’m keeping approval held until both land and are reviewed at the resulting head.