[{"data":1,"prerenderedAt":1469},["ShallowReactive",2],{"\u002Fdocs\u002Fcore-concepts\u002Faccess-control":3},{"id":4,"title":5,"body":6,"description":1462,"extension":1463,"meta":1464,"navigation":60,"path":1465,"seo":1466,"stem":1467,"__hash__":1468},"docs\u002Fdocs\u002F2.core-concepts\u002F5.access-control.md","Access Control",{"type":7,"value":8,"toc":1447},"minimark",[9,13,27,281,284,309,314,317,487,513,516,553,557,564,571,583,600,606,616,666,673,679,689,722,728,748,754,775,781,784,1061,1072,1076,1079,1218,1245,1252,1354,1358,1361,1368,1398,1402,1408,1412,1443],[10,11,5],"h1",{"id":12},"access-control",[14,15,16,17,21,22,26],"p",{},"Mango has no accounts, roles or login of its own. ",[18,19,20],"strong",{},"Your application decides who the caller is; Mango enforces what that caller may see and do."," The bridge between the two is an opaque ",[23,24,25],"code",{},"user"," object and a chain of middlewares.",[28,29,34],"pre",{"className":30,"code":31,"language":32,"meta":33,"style":33},"language-python shiki shiki-themes github-dark","from mango.middleware import CollectionAccess, RowFilter, RedactFields, DenyTools, AuditLog\n\nROLE_COLLECTIONS = {\"analyst\": [\"orders\", \"products\"], \"finance\": [\"orders\", \"invoices\"], \"admin\": \"*\"}\n\nagent.use(CollectionAccess(lambda user: ROLE_COLLECTIONS[user.role]))\nagent.use(RowFilter(\"orders\", lambda user: {} if user.role == \"admin\" else {\"region\": user.region}))\nagent.use(RedactFields(lambda user: [] if user.role == \"admin\" else [\"customers.email\", \"customers.phone\"]))\nagent.use(DenyTools(lambda user: [] if user.role == \"admin\" else [\"save_text_memory\"]))\nagent.use(AuditLog())\n\nanswer = await agent.ask(\"how many orders this month?\", user=current_user)\n","python","",[23,35,36,55,62,120,125,142,180,215,242,248,253],{"__ignoreMap":33},[37,38,41,45,49,52],"span",{"class":39,"line":40},"line",1,[37,42,44],{"class":43},"snl16","from",[37,46,48],{"class":47},"s95oV"," mango.middleware ",[37,50,51],{"class":43},"import",[37,53,54],{"class":47}," CollectionAccess, RowFilter, RedactFields, DenyTools, AuditLog\n",[37,56,58],{"class":39,"line":57},2,[37,59,61],{"emptyLinePlaceholder":60},true,"\n",[37,63,65,69,72,75,79,82,85,88,91,94,97,99,101,103,106,108,111,114,117],{"class":39,"line":64},3,[37,66,68],{"class":67},"sDLfK","ROLE_COLLECTIONS",[37,70,71],{"class":43}," =",[37,73,74],{"class":47}," {",[37,76,78],{"class":77},"sU2Wk","\"analyst\"",[37,80,81],{"class":47},": [",[37,83,84],{"class":77},"\"orders\"",[37,86,87],{"class":47},", ",[37,89,90],{"class":77},"\"products\"",[37,92,93],{"class":47},"], ",[37,95,96],{"class":77},"\"finance\"",[37,98,81],{"class":47},[37,100,84],{"class":77},[37,102,87],{"class":47},[37,104,105],{"class":77},"\"invoices\"",[37,107,93],{"class":47},[37,109,110],{"class":77},"\"admin\"",[37,112,113],{"class":47},": ",[37,115,116],{"class":77},"\"*\"",[37,118,119],{"class":47},"}\n",[37,121,123],{"class":39,"line":122},4,[37,124,61],{"emptyLinePlaceholder":60},[37,126,128,131,134,137,139],{"class":39,"line":127},5,[37,129,130],{"class":47},"agent.use(CollectionAccess(",[37,132,133],{"class":43},"lambda",[37,135,136],{"class":47}," user: ",[37,138,68],{"class":67},[37,140,141],{"class":47},"[user.role]))\n",[37,143,145,148,150,152,154,157,160,163,166,169,172,174,177],{"class":39,"line":144},6,[37,146,147],{"class":47},"agent.use(RowFilter(",[37,149,84],{"class":77},[37,151,87],{"class":47},[37,153,133],{"class":43},[37,155,156],{"class":47}," user: {} ",[37,158,159],{"class":43},"if",[37,161,162],{"class":47}," user.role ",[37,164,165],{"class":43},"==",[37,167,168],{"class":77}," \"admin\"",[37,170,171],{"class":43}," else",[37,173,74],{"class":47},[37,175,176],{"class":77},"\"region\"",[37,178,179],{"class":47},": user.region}))\n",[37,181,183,186,188,191,193,195,197,199,201,204,207,209,212],{"class":39,"line":182},7,[37,184,185],{"class":47},"agent.use(RedactFields(",[37,187,133],{"class":43},[37,189,190],{"class":47}," user: [] ",[37,192,159],{"class":43},[37,194,162],{"class":47},[37,196,165],{"class":43},[37,198,168],{"class":77},[37,200,171],{"class":43},[37,202,203],{"class":47}," [",[37,205,206],{"class":77},"\"customers.email\"",[37,208,87],{"class":47},[37,210,211],{"class":77},"\"customers.phone\"",[37,213,214],{"class":47},"]))\n",[37,216,218,221,223,225,227,229,231,233,235,237,240],{"class":39,"line":217},8,[37,219,220],{"class":47},"agent.use(DenyTools(",[37,222,133],{"class":43},[37,224,190],{"class":47},[37,226,159],{"class":43},[37,228,162],{"class":47},[37,230,165],{"class":43},[37,232,168],{"class":77},[37,234,171],{"class":43},[37,236,203],{"class":47},[37,238,239],{"class":77},"\"save_text_memory\"",[37,241,214],{"class":47},[37,243,245],{"class":39,"line":244},9,[37,246,247],{"class":47},"agent.use(AuditLog())\n",[37,249,251],{"class":39,"line":250},10,[37,252,61],{"emptyLinePlaceholder":60},[37,254,256,259,262,265,268,271,273,276,278],{"class":39,"line":255},11,[37,257,258],{"class":47},"answer ",[37,260,261],{"class":43},"=",[37,263,264],{"class":43}," await",[37,266,267],{"class":47}," agent.ask(",[37,269,270],{"class":77},"\"how many orders this month?\"",[37,272,87],{"class":47},[37,274,25],{"class":275},"s9osk",[37,277,261],{"class":43},[37,279,280],{"class":47},"current_user)\n",[14,282,283],{},"Three things to notice:",[285,286,287,303,306],"ul",{},[288,289,290,293,294,297,298,302],"li",{},[23,291,292],{},"current_user"," is ",[18,295,296],{},"your"," object — a Django user, a dict decoded from a JWT, a dataclass. Mango never reads it; only the functions ",[299,300,301],"em",{},"you"," wrote do.",[288,304,305],{},"Every built-in takes a function of the user, so role logic lives in your code, not in a configuration format Mango invented.",[288,307,308],{},"A blocked call does not crash the turn. It becomes a tool error the LLM relays to the person (\"this data is not available to you\") without retrying or working around it.",[310,311,313],"h2",{"id":312},"how-enforcement-works","How enforcement works",[14,315,316],{},"The agent loop has fixed interception points. Each registered middleware is called at each point, in registration order:",[318,319,320,336],"table",{},[321,322,323],"thead",{},[324,325,326,330,333],"tr",{},[327,328,329],"th",{},"Hook",[327,331,332],{},"Called",[327,334,335],{},"Can",[337,338,339,353,366,379,403,416,433,446,459,472],"tbody",{},[324,340,341,347,350],{},[342,343,344],"td",{},[23,345,346],{},"before_turn(ctx)",[342,348,349],{},"once, before anything else",[342,351,352],{},"block the whole turn (budgets)",[324,354,355,360,363],{},[342,356,357],{},[23,358,359],{},"filter_tools(ctx, tool_defs)",[342,361,362],{},"once per turn",[342,364,365],{},"hide tools from the LLM",[324,367,368,373,376],{},[342,369,370],{},[23,371,372],{},"before_tool(ctx, tool_name, args)",[342,374,375],{},"before every tool call",[342,377,378],{},"rewrite the args or block the call",[324,380,381,386,396],{},[342,382,383],{},[23,384,385],{},"before_query(ctx, query)",[342,387,388,389,392,393],{},"before every ",[23,390,391],{},"run_mql"," \u002F ",[23,394,395],{},"explain_query",[342,397,398,399,402],{},"rewrite the ",[23,400,401],{},"QueryRequest"," or block it",[324,404,405,410,413],{},[342,406,407],{},[23,408,409],{},"after_query(ctx, query, rows)",[342,411,412],{},"after the rows come back",[342,414,415],{},"mask or drop rows",[324,417,418,423,426],{},[342,419,420],{},[23,421,422],{},"after_tool(ctx, tool_name, args, result)",[342,424,425],{},"after every tool call",[342,427,428,429,432],{},"rewrite any ",[23,430,431],{},"ToolResult"," (schema, samples, stats)",[324,434,435,440,443],{},[342,436,437],{},[23,438,439],{},"before_answer(ctx, text)",[342,441,442],{},"before the final answer is returned",[342,444,445],{},"rewrite the text (DLP, disclaimers)",[324,447,448,453,456],{},[342,449,450],{},[23,451,452],{},"after_turn(ctx, stats)",[342,454,455],{},"when the turn is over",[342,457,458],{},"audit",[324,460,461,466,469],{},[342,462,463],{},[23,464,465],{},"filter_schema(ctx, schema)",[342,467,468],{},"when the per-turn prompt is built",[342,470,471],{},"hide collections\u002Ffields from the prompt",[324,473,474,479,481],{},[342,475,476],{},[23,477,478],{},"collection_policy(ctx)",[342,480,362],{},[342,482,483,484],{},"restrict collections ",[299,485,486],{},"inside the runner",[14,488,489,492,493,496,497,87,499,87,502,87,505,508,509,512],{},[23,490,491],{},"ctx"," is a ",[23,494,495],{},"TurnContext"," with ",[23,498,25],{},[23,500,501],{},"question",[23,503,504],{},"session_id",[23,506,507],{},"turn_id"," and a ",[23,510,511],{},"state"," dict middlewares can share within the turn.",[14,514,515],{},"Two hooks deserve a note because they are what makes the restrictions airtight:",[285,517,518,541],{},[288,519,520,525,526,529,530,87,533,536,537,540],{},[18,521,522],{},[23,523,524],{},"collection_policy"," does not just check the tool call: the policy it returns is applied by ",[23,527,528],{},"MongoRunner"," itself, on listing, introspection, profiling and execution — including collections pulled in through ",[23,531,532],{},"$lookup",[23,534,535],{},"$graphLookup"," and ",[23,538,539],{},"$unionWith",". A pipeline written by the LLM cannot reach around it.",[288,542,543,548,549,552],{},[18,544,545],{},[23,546,547],{},"filter_schema"," governs what the ",[299,550,551],{},"prompt"," shows. Hidden collections and masked fields do not appear in the schema section, in value hints, or in memory examples — the LLM never learns they exist.",[310,554,556],{"id":555},"built-in-middlewares","Built-in middlewares",[14,558,559,560,563],{},"All live in ",[23,561,562],{},"mango.middleware",".",[565,566,568],"h3",{"id":567},"collectionaccesscollections_for",[23,569,570],{},"CollectionAccess(collections_for)",[14,572,573,576,577,392,579,582],{},[23,574,575],{},"collections_for(user)"," returns a list of collection names, or ",[23,578,116],{},[23,580,581],{},"None"," for unrestricted access.",[14,584,585,586,589,590,592,593,595,596,599],{},"Hidden collections are invisible everywhere: ",[23,587,588],{},"list_collections",", the prompt schema, value hints, memory examples, and every query — ",[23,591,532],{},"\u002F",[23,594,539],{}," targets included. ",[23,597,598],{},"system.*"," collections are always hidden.",[565,601,603],{"id":602},"rowfiltercollection-filter_for",[23,604,605],{},"RowFilter(collection, filter_for)",[14,607,608,611,612,615],{},[23,609,610],{},"filter_for(user)"," returns an MQL filter document (",[23,613,614],{},"{}"," for no restriction). It is:",[285,617,618,631,638,661],{},[288,619,620,621,87,624,536,627,630],{},"AND-ed into the filter of ",[23,622,623],{},"find",[23,625,626],{},"count",[23,628,629],{},"distinct"," on that collection;",[288,632,633,634,637],{},"prepended as ",[23,635,636],{},"$match"," to aggregation pipelines on that collection;",[288,639,640,641,536,643,645,646,592,649,652,653,656,657,660],{},"pushed into ",[23,642,532],{},[23,644,539],{}," stages that read that collection — a ",[23,647,648],{},"localField",[23,650,651],{},"foreignField"," lookup is rewritten to the pipeline form (",[23,654,655],{},"let"," + ",[23,658,659],{},"$expr",");",[288,662,663,665],{},[23,664,535],{}," on that collection is denied, because it has no place to host a filter.",[14,667,668,669,672],{},"Register one ",[23,670,671],{},"RowFilter"," per collection you need to scope.",[565,674,676],{"id":675},"redactfieldsfields_for",[23,677,678],{},"RedactFields(fields_for)",[14,680,681,684,685,688],{},[23,682,683],{},"fields_for(user)"," returns dotted ",[23,686,687],{},"\"collection.field.path\""," entries. For those fields:",[285,690,691,706,712,719],{},[288,692,693,694,697,698,701,702,705],{},"a query that references them anywhere (filter, projection, sort, ",[23,695,696],{},"$group"," key, ",[23,699,700],{},"$project"," expression, ",[23,703,704],{},"distinct_field",") is denied;",[288,707,708,711],{},[23,709,710],{},"inspect_field"," on them is denied;",[288,713,714,715,718],{},"any value that still reaches a tool result — query rows, sample documents, schema sample values — is replaced with ",[23,716,717],{},"[REDACTED]",";",[288,720,721],{},"their sample values are stripped from the prompt schema.",[565,723,725],{"id":724},"denytoolstools_for",[23,726,727],{},"DenyTools(tools_for)",[14,729,730,733,734,737,738,87,741,536,744,747],{},[23,731,732],{},"tools_for(user)"," returns tool names. Denied tools are removed from the list offered to the LLM ",[299,735,736],{},"and"," refused if called anyway. The names ",[23,739,740],{},"memory_train",[23,742,743],{},"memory_import",[23,745,746],{},"memory_export"," gate the REST memory endpoints the same way.",[565,749,751],{"id":750},"budgetmax_turns_per_daynone-max_queries_per_daynone-key_fornone",[23,752,753],{},"Budget(max_turns_per_day=None, max_queries_per_day=None, key_for=None)",[14,755,756,757,760,761,87,764,87,767,770,771,774],{},"Per-user daily caps on turns and executed queries. Counters live in process memory and reset at UTC midnight; behind several workers put the cap in your gateway instead. ",[23,758,759],{},"key_for(user)"," identifies the user (defaults to ",[23,762,763],{},"id",[23,765,766],{},"sub",[23,768,769],{},"email"," or ",[23,772,773],{},"username",").",[565,776,778],{"id":777},"auditlogsinknone-key_fornone-logger_namemangoaudit",[23,779,780],{},"AuditLog(sink=None, key_for=None, logger_name=\"mango.audit\")",[14,782,783],{},"Emits one event per executed query, per denial and per turn:",[28,785,789],{"className":786,"code":787,"language":788,"meta":33,"style":33},"language-json shiki shiki-themes github-dark","{\"event\": \"query\",  \"ts\": \"…\", \"user\": \"ana\", \"session_id\": \"…\", \"turn_id\": \"…\", \"collection\": \"orders\", \"operation\": \"aggregate\", \"query\": \"{…}\", \"row_count\": 12}\n{\"event\": \"denied\", \"ts\": \"…\", \"user\": \"ana\", \"session_id\": \"…\", \"turn_id\": \"…\", \"tool\": \"run_mql\", \"reason\": \"Collection 'salaries' is not accessible to this user.\"}\n{\"event\": \"turn\",   \"ts\": \"…\", \"user\": \"ana\", \"session_id\": \"…\", \"turn_id\": \"…\", \"question\": \"…\", \"iterations\": 3, \"input_tokens\": 4120, \"output_tokens\": 210, \"tool_calls\": [\"run_mql\"], \"retries\": 0, \"denied\": 1}\n","json",[23,790,791,883,948],{"__ignoreMap":33},[37,792,793,796,799,801,804,807,810,812,815,817,820,822,825,827,830,832,834,836,839,841,843,845,848,850,852,854,857,859,862,864,866,868,871,873,876,878,881],{"class":39,"line":40},[37,794,795],{"class":47},"{",[37,797,798],{"class":67},"\"event\"",[37,800,113],{"class":47},[37,802,803],{"class":77},"\"query\"",[37,805,806],{"class":47},",  ",[37,808,809],{"class":67},"\"ts\"",[37,811,113],{"class":47},[37,813,814],{"class":77},"\"…\"",[37,816,87],{"class":47},[37,818,819],{"class":67},"\"user\"",[37,821,113],{"class":47},[37,823,824],{"class":77},"\"ana\"",[37,826,87],{"class":47},[37,828,829],{"class":67},"\"session_id\"",[37,831,113],{"class":47},[37,833,814],{"class":77},[37,835,87],{"class":47},[37,837,838],{"class":67},"\"turn_id\"",[37,840,113],{"class":47},[37,842,814],{"class":77},[37,844,87],{"class":47},[37,846,847],{"class":67},"\"collection\"",[37,849,113],{"class":47},[37,851,84],{"class":77},[37,853,87],{"class":47},[37,855,856],{"class":67},"\"operation\"",[37,858,113],{"class":47},[37,860,861],{"class":77},"\"aggregate\"",[37,863,87],{"class":47},[37,865,803],{"class":67},[37,867,113],{"class":47},[37,869,870],{"class":77},"\"{…}\"",[37,872,87],{"class":47},[37,874,875],{"class":67},"\"row_count\"",[37,877,113],{"class":47},[37,879,880],{"class":67},"12",[37,882,119],{"class":47},[37,884,885,887,889,891,894,896,898,900,902,904,906,908,910,912,914,916,918,920,922,924,926,928,931,933,936,938,941,943,946],{"class":39,"line":57},[37,886,795],{"class":47},[37,888,798],{"class":67},[37,890,113],{"class":47},[37,892,893],{"class":77},"\"denied\"",[37,895,87],{"class":47},[37,897,809],{"class":67},[37,899,113],{"class":47},[37,901,814],{"class":77},[37,903,87],{"class":47},[37,905,819],{"class":67},[37,907,113],{"class":47},[37,909,824],{"class":77},[37,911,87],{"class":47},[37,913,829],{"class":67},[37,915,113],{"class":47},[37,917,814],{"class":77},[37,919,87],{"class":47},[37,921,838],{"class":67},[37,923,113],{"class":47},[37,925,814],{"class":77},[37,927,87],{"class":47},[37,929,930],{"class":67},"\"tool\"",[37,932,113],{"class":47},[37,934,935],{"class":77},"\"run_mql\"",[37,937,87],{"class":47},[37,939,940],{"class":67},"\"reason\"",[37,942,113],{"class":47},[37,944,945],{"class":77},"\"Collection 'salaries' is not accessible to this user.\"",[37,947,119],{"class":47},[37,949,950,952,954,956,959,962,964,966,968,970,972,974,976,978,980,982,984,986,988,990,992,994,997,999,1001,1003,1006,1008,1011,1013,1016,1018,1021,1023,1026,1028,1031,1033,1036,1038,1040,1042,1045,1047,1050,1052,1054,1056,1059],{"class":39,"line":64},[37,951,795],{"class":47},[37,953,798],{"class":67},[37,955,113],{"class":47},[37,957,958],{"class":77},"\"turn\"",[37,960,961],{"class":47},",   ",[37,963,809],{"class":67},[37,965,113],{"class":47},[37,967,814],{"class":77},[37,969,87],{"class":47},[37,971,819],{"class":67},[37,973,113],{"class":47},[37,975,824],{"class":77},[37,977,87],{"class":47},[37,979,829],{"class":67},[37,981,113],{"class":47},[37,983,814],{"class":77},[37,985,87],{"class":47},[37,987,838],{"class":67},[37,989,113],{"class":47},[37,991,814],{"class":77},[37,993,87],{"class":47},[37,995,996],{"class":67},"\"question\"",[37,998,113],{"class":47},[37,1000,814],{"class":77},[37,1002,87],{"class":47},[37,1004,1005],{"class":67},"\"iterations\"",[37,1007,113],{"class":47},[37,1009,1010],{"class":67},"3",[37,1012,87],{"class":47},[37,1014,1015],{"class":67},"\"input_tokens\"",[37,1017,113],{"class":47},[37,1019,1020],{"class":67},"4120",[37,1022,87],{"class":47},[37,1024,1025],{"class":67},"\"output_tokens\"",[37,1027,113],{"class":47},[37,1029,1030],{"class":67},"210",[37,1032,87],{"class":47},[37,1034,1035],{"class":67},"\"tool_calls\"",[37,1037,81],{"class":47},[37,1039,935],{"class":77},[37,1041,93],{"class":47},[37,1043,1044],{"class":67},"\"retries\"",[37,1046,113],{"class":47},[37,1048,1049],{"class":67},"0",[37,1051,87],{"class":47},[37,1053,893],{"class":67},[37,1055,113],{"class":47},[37,1057,1058],{"class":67},"1",[37,1060,119],{"class":47},[14,1062,1063,1064,1067,1068,1071],{},"By default they are logged as JSON lines on the ",[23,1065,1066],{},"mango.audit"," logger; pass ",[23,1069,1070],{},"sink=callable"," to ship them anywhere.",[310,1073,1075],{"id":1074},"custom-rules","Custom rules",[14,1077,1078],{},"Anything the built-ins do not cover is a function on a hook, registered with a decorator:",[28,1080,1082],{"className":30,"code":1081,"language":32,"meta":33,"style":33},"from mango.middleware import AccessDenied\n\n@agent.before_query\ndef interns_cannot_see_salaries(ctx, query):\n    if ctx.user.role == \"intern\" and query.collection == \"salaries\":\n        raise AccessDenied(\"salaries are not available to interns\")\n    return query\n\n@agent.before_answer\ndef add_disclaimer(ctx, text):\n    return text if ctx.user.role == \"admin\" else f\"{text}\\n\\n_Figures exclude restricted data._\"\n",[23,1083,1084,1095,1099,1105,1116,1143,1157,1165,1169,1174,1184],{"__ignoreMap":33},[37,1085,1086,1088,1090,1092],{"class":39,"line":40},[37,1087,44],{"class":43},[37,1089,48],{"class":47},[37,1091,51],{"class":43},[37,1093,1094],{"class":47}," AccessDenied\n",[37,1096,1097],{"class":39,"line":57},[37,1098,61],{"emptyLinePlaceholder":60},[37,1100,1101],{"class":39,"line":64},[37,1102,1104],{"class":1103},"svObZ","@agent.before_query\n",[37,1106,1107,1110,1113],{"class":39,"line":122},[37,1108,1109],{"class":43},"def",[37,1111,1112],{"class":1103}," interns_cannot_see_salaries",[37,1114,1115],{"class":47},"(ctx, query):\n",[37,1117,1118,1121,1124,1126,1129,1132,1135,1137,1140],{"class":39,"line":127},[37,1119,1120],{"class":43},"    if",[37,1122,1123],{"class":47}," ctx.user.role ",[37,1125,165],{"class":43},[37,1127,1128],{"class":77}," \"intern\"",[37,1130,1131],{"class":43}," and",[37,1133,1134],{"class":47}," query.collection ",[37,1136,165],{"class":43},[37,1138,1139],{"class":77}," \"salaries\"",[37,1141,1142],{"class":47},":\n",[37,1144,1145,1148,1151,1154],{"class":39,"line":144},[37,1146,1147],{"class":43},"        raise",[37,1149,1150],{"class":47}," AccessDenied(",[37,1152,1153],{"class":77},"\"salaries are not available to interns\"",[37,1155,1156],{"class":47},")\n",[37,1158,1159,1162],{"class":39,"line":182},[37,1160,1161],{"class":43},"    return",[37,1163,1164],{"class":47}," query\n",[37,1166,1167],{"class":39,"line":217},[37,1168,61],{"emptyLinePlaceholder":60},[37,1170,1171],{"class":39,"line":244},[37,1172,1173],{"class":1103},"@agent.before_answer\n",[37,1175,1176,1178,1181],{"class":39,"line":250},[37,1177,1109],{"class":43},[37,1179,1180],{"class":1103}," add_disclaimer",[37,1182,1183],{"class":47},"(ctx, text):\n",[37,1185,1186,1188,1191,1193,1195,1197,1199,1201,1204,1207,1209,1212,1215],{"class":39,"line":255},[37,1187,1161],{"class":43},[37,1189,1190],{"class":47}," text ",[37,1192,159],{"class":43},[37,1194,1123],{"class":47},[37,1196,165],{"class":43},[37,1198,168],{"class":77},[37,1200,171],{"class":43},[37,1202,1203],{"class":43}," f",[37,1205,1206],{"class":77},"\"",[37,1208,795],{"class":67},[37,1210,1211],{"class":47},"text",[37,1213,1214],{"class":67},"}\\n\\n",[37,1216,1217],{"class":77},"_Figures exclude restricted data._\"\n",[14,1219,1220,1221,87,1224,87,1227,87,1230,87,1233,87,1236,87,1239,87,1242,563],{},"Available decorators: ",[23,1222,1223],{},"@agent.before_turn",[23,1225,1226],{},"@agent.filter_tools",[23,1228,1229],{},"@agent.before_tool",[23,1231,1232],{},"@agent.before_query",[23,1234,1235],{},"@agent.after_query",[23,1237,1238],{},"@agent.after_tool",[23,1240,1241],{},"@agent.before_answer",[23,1243,1244],{},"@agent.after_turn",[14,1246,1247,1248,1251],{},"For anything stateful or multi-hook, subclass ",[23,1249,1250],{},"Middleware",":",[28,1253,1255],{"className":30,"code":1254,"language":32,"meta":33,"style":33},"from mango.middleware import Middleware, AccessDenied\n\nclass BusinessHours(Middleware):\n    def before_turn(self, ctx):\n        if ctx.user.role == \"contractor\" and not 9 \u003C= datetime.now().hour \u003C 18:\n            raise AccessDenied(\"contractors may query only during business hours\")\n\nagent.use(BusinessHours())\n",[23,1256,1257,1268,1272,1288,1299,1333,1345,1349],{"__ignoreMap":33},[37,1258,1259,1261,1263,1265],{"class":39,"line":40},[37,1260,44],{"class":43},[37,1262,48],{"class":47},[37,1264,51],{"class":43},[37,1266,1267],{"class":47}," Middleware, AccessDenied\n",[37,1269,1270],{"class":39,"line":57},[37,1271,61],{"emptyLinePlaceholder":60},[37,1273,1274,1277,1280,1283,1285],{"class":39,"line":64},[37,1275,1276],{"class":43},"class",[37,1278,1279],{"class":1103}," BusinessHours",[37,1281,1282],{"class":47},"(",[37,1284,1250],{"class":1103},[37,1286,1287],{"class":47},"):\n",[37,1289,1290,1293,1296],{"class":39,"line":122},[37,1291,1292],{"class":43},"    def",[37,1294,1295],{"class":1103}," before_turn",[37,1297,1298],{"class":47},"(self, ctx):\n",[37,1300,1301,1304,1306,1308,1311,1313,1316,1319,1322,1325,1328,1331],{"class":39,"line":127},[37,1302,1303],{"class":43},"        if",[37,1305,1123],{"class":47},[37,1307,165],{"class":43},[37,1309,1310],{"class":77}," \"contractor\"",[37,1312,1131],{"class":43},[37,1314,1315],{"class":43}," not",[37,1317,1318],{"class":67}," 9",[37,1320,1321],{"class":43}," \u003C=",[37,1323,1324],{"class":47}," datetime.now().hour ",[37,1326,1327],{"class":43},"\u003C",[37,1329,1330],{"class":67}," 18",[37,1332,1142],{"class":47},[37,1334,1335,1338,1340,1343],{"class":39,"line":144},[37,1336,1337],{"class":43},"            raise",[37,1339,1150],{"class":47},[37,1341,1342],{"class":77},"\"contractors may query only during business hours\"",[37,1344,1156],{"class":47},[37,1346,1347],{"class":39,"line":182},[37,1348,61],{"emptyLinePlaceholder":60},[37,1350,1351],{"class":39,"line":217},[37,1352,1353],{"class":47},"agent.use(BusinessHours())\n",[310,1355,1357],{"id":1356},"what-a-denial-looks-like","What a denial looks like",[14,1359,1360],{},"From the LLM's side, a blocked tool call returns:",[28,1362,1366],{"className":1363,"code":1365,"language":1211},[1364],"language-text","[ACCESS DENIED] Tool 'run_mql' was blocked by the access policy.\nReason: Collection 'salaries' is not accessible to this user.\nDo not retry this call and do not try to work around the restriction …\n",[23,1367,1365],{"__ignoreMap":33},[14,1369,1370,1371,1374,1375,1378,1379,1382,1383,1386,1387,1390,1391,1394,1395,563],{},"From the client's side, the ",[23,1372,1373],{},"tool_result"," SSE event carries that text in ",[23,1376,1377],{},"preview",", with ",[23,1380,1381],{},"success: false",", and the final answer explains the limit in the user's language. A denial raised in ",[23,1384,1385],{},"before_turn"," ends the turn immediately: ",[23,1388,1389],{},"ask()"," raises ",[23,1392,1393],{},"AccessDenied",", and the HTTP server sends ",[23,1396,1397],{},"{\"type\": \"error\", \"code\": \"access_denied\", \"message\": …}",[310,1399,1401],{"id":1400},"sessions-and-users","Sessions and users",[14,1403,1404,1405,563],{},"Middlewares receive the user on every call, so permissions can change between turns. The HTTP layer additionally binds each session to the user that created it: presenting a session id with a different user is refused with ",[23,1406,1407],{},"403",[310,1409,1411],{"id":1410},"limits","Limits",[285,1413,1414,1427,1433],{},[288,1415,1416,1418,1419,592,1421,1423,1424,1426],{},[23,1417,671],{}," rewrites ",[23,1420,648],{},[23,1422,651],{}," lookups to ",[23,1425,659],{}," joins, which can be slower than the simple form on large, unindexed collections.",[288,1428,1429,1432],{},[23,1430,1431],{},"Budget"," is process-local.",[288,1434,1435,1436,1438,1439,1442],{},"Middlewares run in registration order; a ",[23,1437,671],{}," registered before a ",[23,1440,1441],{},"CollectionAccess"," still works, but keep access first for readability.",[1444,1445,1446],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":33,"searchDepth":57,"depth":57,"links":1448},[1449,1450,1458,1459,1460,1461],{"id":312,"depth":57,"text":313},{"id":555,"depth":57,"text":556,"children":1451},[1452,1453,1454,1455,1456,1457],{"id":567,"depth":64,"text":570},{"id":602,"depth":64,"text":605},{"id":675,"depth":64,"text":678},{"id":724,"depth":64,"text":727},{"id":750,"depth":64,"text":753},{"id":777,"depth":64,"text":780},{"id":1074,"depth":57,"text":1075},{"id":1356,"depth":57,"text":1357},{"id":1400,"depth":57,"text":1401},{"id":1410,"depth":57,"text":1411},"Per-user permissions with middlewares — your app authenticates, Mango enforces.","md",{},"\u002Fdocs\u002Fcore-concepts\u002Faccess-control",{"title":5,"description":1462},"docs\u002F2.core-concepts\u002F5.access-control","uuhx2_vjpQWV2e2KW0eJYgk_PufeC6KDmLGJf-E9tgM",1789553694764]