feat(gateway): grant scopes by verified identity (#121531)

* feat(gateway): grant scopes by verified identity

* chore(plugin-sdk): refresh API baseline

* test(agents): isolate transcript repair mocks
This commit is contained in:
Peter Steinberger
2026-08-10 03:19:45 -07:00
committed by GitHub
parent e97c3450a2
commit c6edd3e80f
17 changed files with 660 additions and 137 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
{
"core": 2288,
"channel": 3718,
"plugin": 4053
"core": 2291,
"channel": 3716,
"plugin": 4030
}
+4 -4
View File
@@ -1,4 +1,4 @@
d7e15b44c0f74635ce38421e72f48f42db439d8c87f29696640ee03c594e73c5 config-baseline.json
25b006d7596d8f607c1ad8d973a60a1a5da4b33fc6c2f56a2b3f02b479d9c503 config-baseline.core.json
9b08f3be75acaf4459d4bdb383b90bd04c408f09db3fdea211518f8d4ae14b84 config-baseline.channel.json
7ce54344ec431d11f8a1ac92a112a9352fc55a4e43be71a08b91f9a06f924fb6 config-baseline.plugin.json
6d70719d3e4b0ff86c96852be6efd7ec86cc85cadd07e27e79b2b22c14aed5b9 config-baseline.json
7832494e638045df79a3ba0d25964fb5100f9d7a1e7d8d38a9174f7b6193bf52 config-baseline.core.json
3a8d0cbdbf9d7d603204fba5b93493050cb76fd9141fcbbec56159ff00158d6f config-baseline.channel.json
4498ac72bb6b9cd5205f7bb8741110a4c53c4e2ec504a1dbb7e7e59c3495f492 config-baseline.plugin.json
+111 -111
View File
@@ -1,151 +1,151 @@
ecdcae2fe366048497da9665ec9b481d80fe2b2b9a63cb4416753c9475487bea module/account-core
40d44524ca231f3e6ef2d5241a963b42050ed8ad3223ea64640fae4dc90e5291 module/account-helpers
89a78a090a26617858696f9c8866faab1ce42a54d9e4c1b9f3b75c28c509182e module/account-core
7dc96f021fd4fb610fa5f71f8cc1610eb5607483cd4f8f2d1409464f052857a3 module/account-helpers
71522995185b956a0cc4927a472cc8d1153e5e998874bfd9a750513175174713 module/account-id
be1c933a14a9e218a28e7544ad2aac009913ea2211413a9c1258417ca0ac2bc9 module/account-resolution
cfc06c91dd67ebaf0fe918434904defff29ce14964a85c2be7b6f2d450aa1ff2 module/account-resolution
4fbb1c87e99399f842a20d75d5e35a4b7064a1b7f02115c23f9a2a7cdcfb57ee module/agent-config-primitives
12f46ee47424ba74f2ea4fdc20c503264aa72671c3eda6d0380c483bee6d2295 module/agent-harness
116a5ea342265a7a2bdb7a8a350f71f417c1a92c953b85ae31d664bd98b11d58 module/agent-harness-runtime
cdf661f6e5b9118ae3b33f0c5e4aec1351ad89b0b61e8b3e02abb7409b4e16da module/agent-media-payload
28b08ba94b446befe54f9ee7a60def97c1d782b96f6c7f964c687d5d8ed06c62 module/agent-runtime
904a765c68c458d67c1d5b7a30ccd9b28e56615f818142dd37a5023c05cfe22a module/agent-scope-runtime
5e5c6ca972df6c19a44608c520a971adf36443c87902c92b3dd985d733308748 module/agent-harness
b3035dcf1d7640de7b1593346db17eed78462cbe08218c8685c794cb0aceff12 module/agent-harness-runtime
64004b88f8d3f85e6c129d99a0b6dc4a06d278e5bd6c95f823417bd44cafbab3 module/agent-media-payload
c2598f0760bf2bfe1731a38ed9d9aa502657ea78b25b41a3f962a9cb51f326a3 module/agent-runtime
9454ff96e4c7d8218de937c5c145300bd1f87820a1bd4676b8c406dce697221c module/agent-scope-runtime
8fecb210e22bce4532b6ab649b09465f0bd2c857a44abf40db7d683d6491e6da module/allow-from
df0e6aca83ecfb53d3b0154b3414e72448be5c46c6b0fb6162f6dc14286c010a module/allowlist-config-edit
525ff6a6681c0c4c3ecddd5f49ce5f9127b3dd4991e0b10f802a208d658ff534 module/approval-auth-runtime
f0287ea003b8af1980d40dcb60c31f2ce54660795bc79b2b36b24a472bebf550 module/approval-client-runtime
19586b5567dd7063f5060096b82bfa50bf8ca84d4ae5b2875f7b34e06596ff46 module/approval-delivery-runtime
62e3bef3b6e2619f8af969d516f1407a11375b92a6aff7aa7d36ca56c0cc294d module/approval-gateway-runtime
eee50f080f9447135fe72dba2102cf0faf9194d37e5fb07e4a0dc78ac719f77d module/approval-handler-adapter-runtime
1d4c93bf614e366bef9dd3a6ee3a1956c237441650e5989ebf81c55bcfa6f37c module/approval-handler-runtime
53d841e27fc25ed2d7dc599bab24273ae99c7cb532db1811362f7f9feffd84cc module/approval-native-runtime
a15ac005239135ce90fabffd8cc9c2c939d8948f99d7ca6940154f9566564ab4 module/approval-reply-runtime
fea44fb4e9675ff4af187e08afa895305e6d68867feae5bdbb5ce87c05ecbe07 module/approval-runtime
5df7f149d8ac7c47acfd9d011b2dccdfd6900fd3277e39c14bb53ea92636c4e5 module/allowlist-config-edit
8d8c09953c7d7c445e009aab2141ff5653085b43d6729ac8dcb8850e414fe964 module/approval-auth-runtime
3c739fda98d03da6680d2cc378556697369c26e5c85e83135863ae100d2d2c83 module/approval-client-runtime
e72705221b0beeedc71f38e38377368987262e0973dd879a0bfd03cf577eb7fc module/approval-delivery-runtime
3d23cb8757e53f99936cc1e659c27b2c4d3a794446cc49f99df8f81d26165ba4 module/approval-gateway-runtime
086135157c1873897569d3356cd8412be45ded7d038d1f8cdaa5349d13ef6ba2 module/approval-handler-adapter-runtime
7a1be47d91917b65c09d07b44342274feeaadcfde11b80e5d2ab811a872ae133 module/approval-handler-runtime
b3216dae8929c399a3500c076e408c097be3e184ece8067be8f97a2f71db708a module/approval-native-runtime
ca5e618bed204e43bb9eb57811c5ab2454538b14a1116e55064aa12f31b3a325 module/approval-reply-runtime
0bf52ea3e5eb08c701eb290791df58a73e93f174c6245a7bb460a6ed01230f86 module/approval-runtime
01ca912836b8dec672f705e294f72d346e778557e4c591317d67558ea7669c0b module/archive
d7e53de63b0ac11a266e4abdc18ba6e9401b80309f5c8f5f6a72a00f65dfe3bd module/boolean-param
0d9b23b23425e07595d7457b3b158197244b7ba8615d9fbc358bad13dddb417f module/channel-actions
10fc989f02a8d081fc34c9953623269a5a1290e2c9db2c85a49a7037a4113c08 module/channel-config-helpers
cb857ebc136481cf9f1e33f8734f866268d571bea98851654671172bef878760 module/channel-actions
a8495ce489f80cdb63a1c2b24398f2f61a104329e2f5a8f525779bf7fcdd9b42 module/channel-config-helpers
c2cc71d5070b6071c51248b0648d1ad1a9468d3737df890adc77ec02025e8853 module/channel-config-primitives
484894c32a2fa1f6ca75538d854569dafbfe48e30c081fc3231f813b3054686c module/channel-config-schema
eb997d5af42ac5363472a6dc47cfd9fe9a4c89e9908fbfac7927e919260b2979 module/channel-contract
b6db3668f533ac91f93a8af7016261646928de5af25a40579bfaf6c5058b9ff1 module/channel-core
c699a67bbbd046bcb4777eb6d288868f2fcfe90ed9faef2abb028d858311b780 module/channel-dm-policy
6412c725137cf23eac91581cab1409b378ff6be9a419616edbf6a177fb7a244d module/channel-entry-contract
421352c351ccf8044f0ba3045c40ba048e6992917f7caf9c92faff2b2e7a4711 module/channel-feedback
c62f1bc2948811e38169abd3d145276c86c0c8a3bbec2b72e70927743045643c module/channel-inbound
378cbd56a4ff711bd748094a145be0f6a3a363f29f608c81f02a3fc3fb2f1edd module/channel-inbound-debounce
5d19511cd325d1d902ee5b4c848de0f15ded5fce22c5a88f057d159c1c88d336 module/channel-ingress-runtime
423e100d8237df2a1cf4208ef2169c60ed3b30a9a0188661c65ade5de94730c2 module/channel-lifecycle
0c235d458307ff0c4569b7066dafc4793cdb28c16424cc50a210aa7fb744941f module/channel-contract
e5624ce3a3dc4d047435b02b2a7cdadcdd6a7391dd6779899cb1c1d31b3a8d2a module/channel-core
7fd7c6da37cf83582b5d1c1bf7cd185c48ec52aab0e75c6fd26dbe247b8acb81 module/channel-dm-policy
0b642564fa67c91b51980bd2bd2a818391ccb39cf3a5187dfbd7405f06ad37ab module/channel-entry-contract
59f6d44b31d4cba5738f16d270c3c3227ae29f51921f491952c4f29d48d0f86c module/channel-feedback
7e1089c7818c13c358c0d93b6aae399e7ab8ab0e3ed199fa2b5f321b59f99a55 module/channel-inbound
8781927f60d726ec2e03f9225c1a34506b16feff45613cc36b5b6885c335f297 module/channel-inbound-debounce
5e4e745555ef5640eb2a24d5af93416d6fbcb6b918f48597990e17bee990085e module/channel-ingress-runtime
e0e00497e22e413a0502529c2c215a4e45aadd4bac259039db4df99cc9ee5990 module/channel-lifecycle
0e47457e38d1df0bd572e1408cde2ca6a788b65205f43c585316b5ad3a8f2f16 module/channel-logging
44935c595d89f51fdcf43ae7cce8e7d2013d86a042be532f835ba8e76c1c694e module/channel-message
ab486c9c181e2a6cb95aadd01245127f3d547ab55e75bab7e1b6fd864932fed0 module/channel-outbound
e04ad39e8ea6d5f0713dbc3cabf265f90cf88b1a6abadccc24ffa52b5da6ac7a module/channel-pairing
9aa661e2e257ff089673e487f79d7fbb7e2a1ca91bbb91143ac44c2cf5b79367 module/channel-plugin-common
2b6f57ab0bd9128551a42c4c4ecd7c3bd4087af9766b86df56ff21f0446c8eff module/channel-policy
5c9491d6fb202d8123638bcbdc7171ebce5ccb30a856efdd028d71696580cbcc module/channel-reply-pipeline
13942428b382fcb7fb5cd92f042596826433a17a6262eb8885101fd50c5f13b3 module/channel-message
bb782210aa586ed37f7f88392f349c99d41a7983e5f9b95d0f2606819f2b90fd module/channel-outbound
b6ad67a681421c9042b695172e19139fcc7c4d7825f7fe3182b53bbcc93c98c2 module/channel-pairing
ab58d4891e0fb3ede16481743c099394fcda26532d222f68dd05574382907f8d module/channel-plugin-common
e3920dd8c81268386bbac71158b4540cd259c69cb097e4bc4fc03b7cf246bf3e module/channel-policy
554425777d686c76a7f26c608e8a914fea84945d6f328f5f41f3100ab42393e0 module/channel-reply-pipeline
482370e60135db9bfaf07f24bab549e5fde09ab265a6061a1f587c5d93929e91 module/channel-runtime-context
4b7d11e77e58bf284f7081df8c4b00e2e658ec19cf645f063db1e506cd7cdf3c module/channel-secret-basic-runtime
574046ce5310620dfa9d989a9bc17121ff2a7cf5d87a20bab7d6f8bf2e3578fb module/channel-secret-runtime
f8f5c78f42f917326012fe80d3dfc31500b1120a17258b6164a75564a608fdc9 module/channel-send-result
84ea037642408624d7c8db58a999d7be6ce733581bb820a8065f76f351f3500f module/channel-setup
aa15810e4698859e5e74a873f3bca85a9e537c83fbed6bb7879b7e606b6766d7 module/channel-status
e988cd8606415fa6a6f49acde7a6b0984408972b2f2cfcde0a5d597d8f030776 module/channel-secret-basic-runtime
70552599e1d2a6959feba95f0bb3155d98ee377027a956414da9c5acba90fcf1 module/channel-secret-runtime
dbb2bf9e63bfe914c3e1d8c0d9e78bdb48d1692e28512a612362d33d24c09863 module/channel-send-result
6fa564bfcb0b84da63b68decbb4653ebd737ac1d7fb202973b57642f01848a0b module/channel-setup
f395f472baedc054431f2376135824ffb9e01ec2784b750b9c67c177aa1238b6 module/channel-status
b227a529438b8765fef0a1af03effeed1b8594152a0e8cfb222a14caa4020a56 module/channel-streaming
b2f920ff4a6b4190e6d6ea0a3effb001751e092f0e3ac0cf296721ff8c383d86 module/channel-streaming-config
fdeffe356c7c4edeec9f8fd03edcadc375eabc7a9412e582b10c3180e3ef40fc module/cli-argv
ad12670dbfe538f8d0ebf4fb2b68080e93a760278278e6b1ce9bb129d4b2d533 module/collection-runtime
b549decd793dab48d8be29c0131be71d08f15cc97a0b22d13f327f5efd797259 module/command-auth
40844dbcc0e4c5d0ab89668d9f90e9c636f99dc4ed0ba10d7b7f2d241a000d5e module/command-auth-native
b99ff6081946a8cc08e8e63d852cb484462808c92add2fb23cb36633583cb66e module/command-detection
9f6332a1db7cd6f2fcb92d8ac2e4af32379d694bfbeeab736f304815214b364a module/command-primitives-runtime
e461ac9cb7520441e5867fe4ff7882341f3d1338b7a0c12934f2e1645ef37f91 module/command-status
ec10d7ce3379d2d2d45dc3e6897b56d092adca57c74d4a27f3855f82437c6e81 module/config-contracts
75a80626b1583434ebeffd6d48c066aaaffdb2c6e215abe98f106c6b927415f4 module/config-mutation
fe1fb6ca8307528dda4fd6c48f180a79840d98ef18be6e633544f351ac0ab120 module/config-runtime
d2ac206c6ed4f1492d4136c704fd55a4aa68de11bc36f610faaf16714bfa4862 module/conversation-runtime
cf56f29cf1a7baa7c72e9d8710521eb935f1bf7c1cc95064cb354c8183dbf962 module/core
3477dabc15a35fcdf64490d0c13d920aa290cf6dccd0077534a1eeda36490a22 module/dedupe-runtime
ad486bd560ee32f2bcb9ecd3e76d5ffb937f855d6fc4656b632908c0906b97de module/command-auth
7155990bfa4f3b93b80308f582be699abc9f6a433a492dcc295882b80f04f7ea module/command-auth-native
dae206921890e3fe3b722b8c76a8fd35d451dd2cf462058074000bbc158943fe module/command-detection
68403cefe336402ee7e2b47ba55367f7e79765bd5f95d726b37a8b3084bf63f9 module/command-primitives-runtime
8a1433ceb3864c14900b1bc8cc149511d491089963bd2d837efd11bc65b65d02 module/command-status
e2e40bee9b8406c004d579df5704223c5cdd2cb84c3f6e8795cee3f8ca1bdae9 module/config-contracts
3719e5143fd7732aa8db717acb0c3b075e15140422ab8b533157f668f85a2080 module/config-mutation
6af580c0b66ef3940b7a96fced371a7afda4912577d45550aa8d62f9447bbb97 module/config-runtime
883cb53ac9dc6bed411f6a4d4a97345365857adc5fc46395c5d05b36beaadd76 module/conversation-runtime
01781ceb5acf5296de41822d7db2d359a9b622e95e6c6ae291b4fe3a900f6dfd module/core
a28621c0a26372f95851fac43729e8ac6198f6d7c09e7d6cb64bbe93fdab93ec module/dedupe-runtime
ebef0e650ab45e44c9335e2b3e15588c968cea6dadd125364a076f9c50ad1e8c module/device-bootstrap
b8d4ff8d1a3f9d28962ad3ebe4215f713fba47a28fbce5f083aae72a9316e6c0 module/diagnostic-runtime
4dc492621bb2ebde58fc4cf9d9d1f2a3ff95411bb88230fc179513725ddb1959 module/directory-runtime
8c4fdb1f24e8828efa7b0810f11f6b28d0334c1e47f5f10372caacd35d1eb9fb module/discord
64adc7f42bebf579531d8e18615b4f2384dcd9f35265f88c5129c10277b12eee module/error-runtime
05ff25c56097b12fd9956115eb4bf9a5171574a2137fd04a4e34f195e3813e5f module/extension-shared
8546c29222f65dffe162ae543657bd632400b2691e0aa7890dfecb7f7f25664c module/diagnostic-runtime
b5f9a2bd44fa90d8c4212f73d12989a55669fe2971513c8bd21fcb173dbb6f35 module/directory-runtime
d41a1bfd64023359a857cda843309d86a32bd1cf4799a8f9027b5de55c5ea292 module/discord
aea6c70b74cf24a9dd34c9e127e39623bb86cae13de809e0103aff456c623eec module/error-runtime
05caf0ebadfd51ae6fda842484392c142d66f47524bded2d4843aaeb6ebe23c0 module/extension-shared
dd9f6e0fd33cc88b22543c1ee30cc09cf4de4d8f30dff7b7f9cebef885c21543 module/gateway-method-runtime
b49f8988d4d336accb053c170da47fc20ac7e6df5b2cdb8c5631e91bdb113b38 module/gateway-runtime
30d5e1a52ccd32b2adfed890f97d6614f79d9c6b524c3405707a849d18341618 module/gateway-runtime
1b1c6bd5bfc0cfb0c5bb9bd97f8ac1928750cb87232a3415dd066cc21d7b48b7 module/group-access
16ca326bf06e43319841dc1c00e952601a5edfac77e337dead2d547c32f459d3 module/health
eba928a25e2e53c039c968265a20d711037810d1f62f9960e7599f55a77315f2 module/hook-runtime
f5e190bbfe0c21e76b7281a73cf5e9db806a1ed6e724fac2fb83ade5ccb827a0 module/inbound-envelope
45fac7ba15b10002f331321336c8c176746a185998b77383c588a98b8948250d module/health
ced757b895c88172b44cd876ac05b49edfd526c566f2576c54ba54fe95c519e5 module/hook-runtime
112224f263b0fe8413dfeef48c2ffe31c3b1b102984eb0e6f2646c3170603465 module/inbound-envelope
4928af5d2509f696b896f53ac790303a0742202dbcdae3e44fe6d1b434a9c1ba module/inbound-event-delivery
b509e536500ffa77d2de7574222bead67122ebe6357f3c06089019977282ab12 module/inbound-reply-dispatch
8e501afc1320c7ec2cf5aa2e28257b5fd4cb918bced23939d63fe3c9f69009cf module/infra-runtime
ebbc8539a60bf3eac8f3936930396501d63172205993c3051557056d926cdfdf module/inbound-reply-dispatch
c76a09319f8417c1380072a7726137379095e023c834757e5737f4a4ad2bd997 module/infra-runtime
ce73721421f1b903dd04ead4df173582e59ea3e9990248102c448b419cc6d272 module/ingress-effect-once
c2a7b1b42422ec85ec6c8655adcf987dbb410574ca8539ea6d259420a47fd296 module/interactive-runtime
c85dfa7f61f04dec284af1516ce9cb923458a54f7827b78f1a507546553f3ed1 module/interactive-runtime
408d257ab5cc4b88a22b7e7595039cb8fc524b261c44141b294fbd0100ba62ee module/json-store
e907fd3a98185f2c261f2aafcaa5a19ee1d7b459d519a498397d629f84c68312 module/lazy-runtime
9874591cf115a4ad2c9a6cecd7fd6d2bb0a5d0a4c435d35b85a1ad3efdb85979 module/logging-core
8cca40cd8f8430d8b9ce3d9d79358e1be8f7474180960c2b40cd57b5c144c1d0 module/logging-core
f1ca4ced4305d0769c2d8cc1291137ac7002fe0e6eaec2c1a71edad2204c8311 module/matrix
ae17382b302c022012418146971c7bf69ed94316c51b62dbbf516e56ab4b327e module/media-local-roots
d32041b6c9d8801c78aa233cd232b0cff13dbe1a340de599ae6e4a005421ecf4 module/media-local-roots
f74d7295fe716aa140aa0bc9300d6259d71dab826de0808fca6bb02592bf5d6e module/media-mime
47a9fa2d97a666a6ed149ed810bbb5561d885503868f28c7fd7e274551c6a43f module/media-runtime
5dd079f78d1d96359e556853e5cd4f82ce10e0564fd82885bfd4cb44b9065c7d module/media-runtime
6a52f93107335f88751704352cc01e62add06f854a5b7d765e2a5ee87c0313b6 module/media-store
3a4e5c9a84a98b012fbe30f298dbe6168a0d0cdcf57b412f809cd413bc826e39 module/media-understanding
d1b7c9a8121e395df2152b46f6cf7c2762065ea48e9b11fe6588d40d9bf370ba module/media-understanding-runtime
dc07d85c16be62a001c9a892b45514ac46147f57b679b5febaad05c83f9464e1 module/meeting-runtime
f457e2035a9ccefcb6010a0a14b415f92772dbead4dacedb796ecb5d564892c9 module/memory-core-host-engine-foundation
e7cd98a974b6bc804a0e96fb96d88103aab32269da713db8282abb475018062d module/memory-host-core
246c8b2812f4353e039de0fc997c58d7b461456d1bf5b04d0aa32fb280a364ea module/media-understanding
639d2ab2837f00621fa657e29b86eeb3b9b672c91a2f28623d754921603dcfd5 module/media-understanding-runtime
36c3dbb38106e9cf2df5fb04afb12de4ddf02c77901b5fd36d3b1e2ad72a5233 module/meeting-runtime
c10be4c48e5ac37b43206b9d55a4306f50ff73735be323ae93f23692e530ee3a module/memory-core-host-engine-foundation
79d2ed79a8696ad1674dc3a503ec6f46cafe669999a569b956462547d5b67d55 module/memory-host-core
1efa0aadc4261d1c6073058cbf3dcc9fa681424819bdd14333e19b249bbc4b18 module/messaging-targets
01fbccef009c2a41162f5327f7ab087d1345b1288e00e342fe7042eb20a24e56 module/model-session-runtime
df57353a6e156973304e7f487dc55c006338d07c10a030e124edff4a993c38f1 module/models-provider-runtime
6e577f2b80c1923eea10467a57cf580bec7c323d365f59a5df3e118b7858cec4 module/native-command-config-runtime
c241f194708a75e6f539b58b7837f1f5eb68b9f205ba9c74d2c5e149f1d742e0 module/native-command-registry
35bc6e2da664788158dbbab1f733975409c4279694bf9b1b81a36a00796cdf2d module/param-readers
2a573fa218a44c27ddc65735c5fb49fe1ef691bea72d52ce502c5afcc45ce47c module/model-session-runtime
6a0eb4b0c0ed099ecc6b490be90d6e15340998bcc3e0cd9e969632d3e3d78c41 module/models-provider-runtime
8c73f1d05264e1d3cb5c0a3b52aafd4669a9633b60122f45dff1a5a3e1812565 module/native-command-config-runtime
e3b6d6bf3bfdc79b9817574db4174dff11ccf306ab521e2547e14a0dc9fff8c7 module/native-command-registry
6fbafa5e99257462e1766d04e9aa08e22229cf132de51924457743d9741a32fd module/param-readers
ca7a56bb1a6169b4cf9befbf5aa21da280a8086fdc49fca4eec520a7a7c98549 module/persistent-dedupe
e0a68ab64db24432eebb162b4aa376d359aece930f6de45ccd0af4e523496a66 module/plugin-config-runtime
88e79074f499c7db6a0bb05ea35f3e9bbcaf44c45ce4c97bb4571de511e6dc9d module/plugin-entry
e3825c22419509217aabd6c7274d1ccc2964b268fd9e813f82531d5ed5b46814 module/plugin-runtime
ff1ef8ff2b2dbf2692fe043d4c0f7c525300cecc6288dba6e5a0c41ced08c0f4 module/provider-auth
17ed49cdbbc73268be7b2c9b39ca9050a4a692bb01811c5bc0fe3cd461e8fd8b module/provider-catalog-runtime
f806b7326c4462fbbfc7407ff5d0eea831dbda4f87f73d0cdb193c1baf9e195d module/plugin-config-runtime
beb6923354b3046a7c552a3476eb9c0c33f3c994de69e4b10a97a3eeaf4ce3fe module/plugin-entry
d54879d527a9de84af4820bae79e66ad207896c595119f12330eca641151a2ce module/plugin-runtime
413b203696ff75c52f12008dfa3af5025e060bee6a5e32bba4af2d3087a67740 module/provider-auth
6798bbe969215d0600d13b429098ca37133fa455d0589890ae79a9fd40a6d37d module/provider-catalog-runtime
8131147d699394bd06503e2ea2f5f1a50b1594a87dded6d118b74a8d0328c8f6 module/proxy-capture
5b0d5033b4517871e32ef909257e716164b0153f6ccb139231b34a551f967af0 module/question-gateway-runtime
3dff46dd2c25f3101d993179358d3e95797cb3c5f9c8359bec8af01c19187607 module/reply-chunking
480ca2a418f25393df5349cbecbb458eab6e5f3ba9fa05f1e2ca69791b3410a0 module/reply-dispatch-runtime
d077971d6208c5459ee2234283cae14ac4cfd74a8362de461800ed64ab5b3bc3 module/question-gateway-runtime
d8836fcad8b49d6f2965cfda99dc31fe3d0229d97ecfeca494dc3bebf3a81be0 module/reply-chunking
ee62553e9d036c1766e86551cee8716886538eea5f300735e1487a7e67db7f9f module/reply-dispatch-runtime
73f861fa3179d5af1159853c5acab0eec7a6c8f9398dcb75ea770e784fca6727 module/reply-history
d3c3aa7f8d77ba6adc07f080628119af0e6b0b27f3a06f1ddd3bc498a5decf81 module/reply-payload
57e6ef7348aae6c866565afd2a5f9bc2760e858f1716832ce509312654102b2b module/reply-runtime
383b1bcefcbcb768dd20a4a2c3e54a96f7a75ad14eb9a3cc4665468b0e1159b5 module/reply-payload
ce7fc1c4130ad37229ed5435542c072877330dcc98d8fc9c9bb8e7fb254ea744 module/reply-runtime
aa07d85d99fdd2b1e0cbe9975fb6dcae66b8bdce2607c6bd5402ae68bb15118c module/root-walk
eee39bd28309cd706671cd86d598706286f99f5987f31615b77af8d81d696fff module/routing
1404e950e93a7adbb2d2e5d3246b31476ea722846a4b2a6cd16f1af9ca0e87f5 module/routing
7877a7e58fa32a64107154e5b714c6d165e96989d4aa5f43e0afac085a187af0 module/run-command
4d0fc71ef378b8eae3ad85e1d4e15b4fbac6f6075b653238dfbb0c9e1ba8327c module/runtime
3269124490363a8063eb6230f703609ec9c4d4f02c137a035b19290aff831592 module/runtime-config-snapshot
9e8651266b12dca1f0231a8d0956cc9142b43ff2b6c300f180a3f7c859bc96e7 module/runtime-env
07e624c859c3f91fe010aac0e6992456e00c033c883ed0fba0760dfac68ab8f1 module/runtime
4aeeb72d62ee0429395b127f9593539c4fe33ed3752c15f9cb1f29a8f3c568e7 module/runtime-config-snapshot
300a1d4b0d0d1b68274510e3dc16b4e336319bd8ac4e8358a0a5251af9e841d7 module/runtime-env
49e9b6a8195c89704eaa80656f176444af7cacbf639b759f41f2c78ae6bfcfd9 module/runtime-group-policy
c25cc3318c66b8f0a2a7114a26e583869e36e8a63ca53839c8b6843adefbb10a module/runtime-store
2aae1e2770ca4970e07745eba226cdb6c93fc9cbad70309706d52ddf31b4bfa7 module/runtime-store
d17862c40825af1ddf0257b44f1e1cbb9c375e8e5ed668fae75d530d1a465cf9 module/secret-file
8e2ac4d3973d8d8ce4478e3440d66ee5c0d9213b0fe9e927c421d14fd31e5e86 module/secret-input
333ee3f8889687fc284902a7ffb2fe9dbb41a9f4e3f7fe1f6b0d8bc330de6225 module/secret-input-runtime
91737225f39e684805fe8bede33933cbb5cefe0c0dbeb0346f69b4a9e29be929 module/secret-ref-runtime
fdc9f9ac16fd4f781d76dd620df2bc580c8892c767f1fcecbcea756ea5e02b5c module/security-runtime
dfbd35157880a46b5697eb83f17ac8ce1bf43f5bcb3027b1e67ce71cded25445 module/session-catalog
ba7577f76ebfa21601ce8116b17b7d5d0f04e305bc1f2c0cea76c0594c61b2ed module/session-discussion
4a906445ca358e6c54d641d7fe4e7d2a8f2f78af8bed7d920174192d5f83ca61 module/session-store-runtime
337f63bf878dad8affd71b8e4e66252653ca4fdbadabd398ce527718deecd5c0 module/setup
ad996e771d8ccf51e1a8944b54d4c0b3695da35617a4c64981bd5e3fdb865f96 module/setup-runtime
9490b04fb0167b8e505e90625e3464a8c21807aa06d8d57b68bf95c7806eee2a module/secret-input-runtime
25ec161b94736377563c08f9cda400a7133434ad72e74eeb553c31542509a214 module/secret-ref-runtime
0f5596e9b55521ebde283839d15cc2f3d491dbff9bb8067ec4e5b7b884677c3a module/security-runtime
73c493015363b289377349367199715d1a282ee9082baa99bb792b15deb9199c module/session-catalog
5a00f670509141d28dcfd9475e2926ad8d4ae6ded2654f9f54933a09d66d4f7f module/session-discussion
834ed405f3c02c20b564ae1d69a0d29dd758e99c9c3a9bef7710cb3e31340601 module/session-store-runtime
2645f8827c4751b57167d67ab4f423a1e7edce079c3a973981a3f753fa0834d1 module/setup
f292edb93a7bc7e499490dc9bf316015775e68edf63be858673b5c2bdbb90799 module/setup-runtime
d0cb4c5abb7484352088f556c1ba7c7b147d7b57977b8246e5cb7187937768b7 module/setup-tools
07ddadba30645d0a3bb8f449cc68eb3c98cde57b3d0a97b646477536f0790509 module/skill-commands-runtime
6a08f3914e9ea2ddabd98cd5bb960419b9cac9a81c111f3c1eccb256d461135c module/speech-settings
05f645dc200055946e7386c2fd022bf9b6c9062e212b086aa22872ab2e8f2429 module/ssrf-policy
ef358b38d1441e23557003c18d33fef9b2c390edab2201f1ad428f1fe80b782f module/ssrf-runtime
cf7f004d754d995bd4989b09a8a539635588c9e679deba65ecc66d9c25e513b2 module/state-paths
58cf4ad2dbf36ac3dfc00ba9dfcf94f7e296a9cc28f1b880a466b99478529663 module/status-helpers
81c3d0a5194ec8bc6ee8635f2910bee69815635ddd6aeef88e988d4effabc0b7 module/skill-commands-runtime
0bbda17999433ca8dd0e01ddffb6760096a3d90813d34c6c371ce9a62e0361d0 module/speech-settings
e5096ff1a61bec0413590be28a9254ff4773c350d5095ff82fb9be0be691f9bc module/ssrf-policy
393b98970842cfff3209c6e146604e9e5a72ea11a87fb3e622a1f0bb36007fae module/ssrf-runtime
bc3f1c2d9aeb4a0890905fcea07d4662bf65a3687a4590cc5c34fca7d55510b4 module/state-paths
6920a1afa8347a4cef78214df539fc5e0e2b46769a814812791b36e507a8ba16 module/status-helpers
f097d0096b21c8a052f0f649b7512ecf2aba4744ae6956f001950e053828b309 module/string-coerce-runtime
718c4ceabc0608514bbd90d12fd20e35a3ae536808193cd79a1d04bfcf862d83 module/telegram-account
0dd45b013da7e164d3d7177332bc268a18104eae9c153416b94a2c981dbdda6b module/telegram-account
aef35bee2502cd6ed8765409b758e452aff8ac9469fd773e6a2a44c9a1bc3f66 module/temp-path
87fa81b9e58d8fc04a4b4202d2d37fca339615f5225687d9db905151439e0f4d module/text-chunking
0e3168845e5021738db84e85768500e54aacee269026da73a6b70f350a29362a module/text-runtime
487f48dfe78f5be60ac16def677905d83ba41090929a99c06f229890c8e818f0 module/tool-plugin
19e1bfca1b06768d86835c0fe416ca8c7c67e63d5f2e00a4ad902a7ada70bb9c module/text-runtime
c4075684cd95ea5a80143cb2e45a7bdc6bca8dc22b8eff394d9c55a0ddcf4c40 module/tool-plugin
dc1a073c59ab61e2789533b777b3f0cb9af689d64a97796b10e8aa82552510db module/tool-results
603abbf9716886fbcdf5cbb3ccac71c59be959695e3ca4822d3e482cf3851381 module/tool-send
dd6611db9ee085fd7c7410e85c4a472b2e3a0dec91df0343aaf6efd81a64cafb module/tool-send
cda105b721d498df23a554c6b68be150b8fe66b8b9172185c31a0b3b0646b1dc module/web-media
f11229ca65444d7580436e75c0e8b6b298369b749ea0003b6123c1541a09fc1f module/webhook-ingress
e7c422d17088a42544f2c9e5b78b3a2a460c2a16c78a34f2da3b089cd6e9f680 module/webhook-request-guards
790b99e8523e0190c8bf908d37f1aee4d1833bc7c96e765f03996eed7d770768 module/webhook-ingress
7f12f9054eabfac5eeff0d06b3e8400a90f7a5424cd4ca36113224306d60081b module/webhook-request-guards
de59e86e126b75d13251cba7ebbe27b44d9b5588785d98df5ff4d6722374c81f module/widget-html
9161b36ec0ab062ea41b363c894fcd672a7727f21cb726739f99f9c184fce69d module/zod
+4
View File
@@ -575,6 +575,9 @@ See [Plugins](/tools/plugin).
// password: "your-password", // or OPENCLAW_GATEWAY_PASSWORD
// trustedProxy: { userHeader: "x-forwarded-user" }, // for mode=trusted-proxy; see /gateway/trusted-proxy-auth
allowTailscale: true,
identityScopes: {
"admin@example.com": ["operator.admin"],
},
rateLimit: {
maxAttempts: 10,
windowMs: 60000,
@@ -661,6 +664,7 @@ See [Plugins](/tools/plugin).
- `gateway.auth.mode: "none"`: explicit no-auth mode. Use only for trusted local loopback setups; this is intentionally not offered by onboarding prompts.
- `gateway.auth.mode: "trusted-proxy"`: delegate browser/user auth to an identity-aware reverse proxy and trust identity headers from `gateway.trustedProxies` (see [Trusted Proxy Auth](/gateway/trusted-proxy-auth)). This mode expects a **non-loopback** proxy source by default; same-host loopback reverse proxies require explicit `gateway.auth.trustedProxy.allowLoopback = true`. Internal same-host callers can use `gateway.auth.password` as a local direct fallback; `gateway.auth.token` remains mutually exclusive with trusted-proxy mode.
- `gateway.auth.allowTailscale`: when `true`, Tailscale Serve identity headers can satisfy Control UI/WebSocket auth (verified via `tailscale whois`). HTTP API endpoints do **not** use that Tailscale header auth; they follow the gateway's normal HTTP auth mode instead. This tokenless flow assumes the gateway host is trusted. Defaults to `true` when `tailscale.mode = "serve"`.
- `gateway.auth.identityScopes`: maps a verified trusted-proxy user or Tailscale WhoIs login to connection-only operator scopes. Email keys match case-insensitively; other identities match exactly. For trusted-proxy Control UI connections, `x-openclaw-scopes` caps device enrollment or upgrade requests and the final device-plus-identity session scopes. Grants do not create or modify pairing records. Token, password, and no-auth connections have no verified identity and receive no grant.
- `gateway.auth.rateLimit`: optional failed-auth limiter. Applies per client IP and per auth scope (shared-secret and device-token are tracked independently). Blocked attempts return `429` + `Retry-After`.
- On the async Tailscale Serve Control UI path, failed attempts for the same `{scope, clientIp}` are serialized before the failure write. Concurrent bad attempts from the same client can therefore trip the limiter on the second request instead of both racing through as plain mismatches.
- `gateway.auth.rateLimit.exemptLoopback` defaults to `true`; set `false` when you intentionally want localhost traffic rate-limited too (for test setups or strict proxy deployments).
+37
View File
@@ -43,6 +43,43 @@ require the `node` role.
Unknown future `operator.*` scopes require an exact match unless the caller
already holds `operator.admin`.
## Identity scope grants
`gateway.auth.identityScopes` grants operator scopes to verified user
identities from trusted-proxy auth or Tailscale WhoIs:
```json5
{
gateway: {
auth: {
identityScopes: {
"admin@example.com": ["operator.admin"],
"operator@example.com": ["operator.read", "operator.write"],
},
},
},
}
```
The key is the verified proxy identity or Tailscale WhoIs login. Email keys
match case-insensitively; non-email identities match exactly. Config validation
rejects scope names outside the closed set above.
Connection authority is resolved in this order:
1. For trusted-proxy Control UI connections, `x-openclaw-scopes` first caps
device enrollment or upgrade requests. Device authorization then establishes
the persistent scopes; a device-less session contributes no self-declared
scopes.
2. OpenClaw unions a matching server-side identity grant with those scopes.
3. OpenClaw applies `x-openclaw-scopes` to the final union as the session cap.
An absent header means no cap; a present-but-empty header yields no scopes.
The result is used for both `hello.auth.scopes` and Gateway method
authorization. Identity grants are session-only: they do not create or modify
pairing records or request a device scope upgrade. Token, password, and no-auth
connections carry no verified identity and receive no grant.
## Method scope is only the first gate
Each Gateway RPC has a least-privilege method scope that decides whether a
+42 -5
View File
@@ -60,6 +60,9 @@ read_when:
auth: {
mode: "trusted-proxy",
identityScopes: {
"admin@company.org": ["operator.admin"],
},
trustedProxy: {
// Header containing authenticated user identity (required)
userHeader: "x-forwarded-user",
@@ -108,6 +111,9 @@ Internal Gateway clients that do not travel through the reverse proxy should use
<ParamField path="gateway.auth.mode" type="string" required>
Must be `"trusted-proxy"`.
</ParamField>
<ParamField path="gateway.auth.identityScopes" type="record<string, string[]>">
Connection-only operator scopes granted to verified trusted-proxy or Tailscale identities. Email keys match case-insensitively; unknown scope names fail config validation.
</ParamField>
<ParamField path="gateway.auth.trustedProxy.userHeader" type="string" required>
Header name containing the authenticated user identity.
</ParamField>
@@ -131,6 +137,37 @@ Internal Gateway clients that do not travel through the reverse proxy should use
Only enable `allowLoopback` when the local reverse proxy is the intended trust boundary. Any local process that can connect to the Gateway can try to send proxy identity headers, so keep direct Gateway access private to the host and require proxy-owned headers such as `x-forwarded-proto`, or a signed assertion header where your proxy supports one.
</Warning>
## Per-identity scope grants
Use `gateway.auth.identityScopes` to give selected verified users additional
operator scopes without widening their persistent device grant:
```json5
{
gateway: {
auth: {
mode: "trusted-proxy",
identityScopes: {
"admin@example.com": ["operator.admin"],
"operator@example.com": ["operator.read", "operator.write"],
},
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
},
}
```
The map key is the verified trusted-proxy identity or Tailscale WhoIs login.
Email matching is case-insensitive; non-email identities match exactly. On each
connection, OpenClaw adds the matching identity scopes to the device-authorized
scopes, then applies an explicit `x-openclaw-scopes` connection cap.
These grants are session-only. They do not create or update device pairing
records and do not trigger device scope-upgrade requests. Token, password, and
no-auth connections do not carry a verified identity and never receive a grant.
## Automatic device approval
Trusted-proxy auth can optionally use the proxy identity as the approval boundary for new browser devices:
@@ -158,7 +195,7 @@ The default is `enabled: false`. When enabled, all of these rules apply:
1. The WebSocket must have authenticated through the `trusted-proxy` method with a non-empty user identity that passed `allowUsers` when an allowlist is configured. Token, password, Tailscale, and unauthenticated connections never use this policy.
2. Only a new Control UI or WebChat browser device can be approved automatically. Any request for an existing device, including a scope upgrade, remains pending for manual approval with `openclaw devices approve <requestId>`.
3. The device is approved with role `operator`. If the connect request includes scopes, the grant is the exact intersection of the requested scopes and `deviceAutoApprove.scopes`. If the request omits scopes, the configured list is granted; when that list is omitted, it defaults to `operator.read`, `operator.write`, and `operator.approvals`. The resulting grant is then additionally capped by the connection's [`x-openclaw-scopes`](#control-ui-pairing-behavior) proxy header when present, so a proxy that narrows a user's scopes also limits the **persistent** device grant, not just the session — a present-but-empty header yields no scopes. This cap applies even when the client omits its own scope list.
4. `operator.admin` is allowed only through explicit listing in `deviceAutoApprove.scopes`. When listed, every proxy-authenticated user can request and automatically receive full admin on a new browser device; requests without scopes receive full admin automatically. `openclaw security audit` reports the CRITICAL `gateway.trusted_proxy_device_auto_approve_admin` finding, and the Gateway logs a warning once at startup. Prefer manual admin approval with `openclaw devices approve` or `openclaw devices rotate` until per-identity roles are available.
4. `operator.admin` is allowed only through explicit listing in `deviceAutoApprove.scopes`. When listed, every proxy-authenticated user can request and automatically receive full admin on a new browser device; requests without scopes receive full admin automatically. `openclaw security audit` reports the CRITICAL `gateway.trusted_proxy_device_auto_approve_admin` finding, and the Gateway logs a warning once at startup. Prefer a targeted [`identityScopes`](#per-identity-scope-grants) admin grant when selected verified users need session admin without a persistent admin device grant.
<Warning>
Enabling this option delegates new browser device enrollment entirely to the reverse-proxy identity. A compromised proxy account can enroll a persistent device with every configured scope. Listing `operator.admin` makes that device a full administrator without manual approval. Keep the Gateway reachable only through the proxy, require strong proxy authentication, overwrite identity headers, and use a narrow `allowUsers` list.
@@ -170,17 +207,17 @@ When `gateway.auth.mode = "trusted-proxy"` is active and the request passes trus
Scope implications:
- Device-less Control UI WebSocket sessions connect but receive no operator scopes by default. OpenClaw clears the requested scope list to `[]` so a session not bound to an approved paired device/token cannot self-declare permissions.
- Device-less Control UI WebSocket sessions cannot self-declare permissions. OpenClaw clears their requested scope list to `[]`, then applies any matching server-side `identityScopes` grant after proxy identity verification.
- If methods fail with `missing scope` after a successful WebSocket connect, use HTTPS so the browser can generate device identity and complete pairing. See [Control UI insecure HTTP](/web/control-ui#insecure-http).
- Older configs that still contain the retired
`gateway.controlUi.dangerouslyDisableDeviceAuth=true` key use the bounded
[Control UI upgrade migration](/web/control-ui#device-pairing-first-connection).
Reverse-proxy scope capping: if your proxy sends `x-openclaw-scopes` on the Control UI WebSocket upgrade request, OpenClaw caps the session scopes to the intersection of the requested scopes and the declared scopes. This header does not grant scopes; it only narrows what the session can hold. When `deviceAutoApprove.enabled` is true, the same cap also applies to the persistent device grant written by [automatic device approval](#automatic-device-approval), so an auto-approved device never holds more than the proxy declared.
Reverse-proxy scope capping: if your proxy sends `x-openclaw-scopes` on the Control UI WebSocket upgrade request, OpenClaw caps device enrollment or upgrade requests and the final union of device-authorized and identity-granted session scopes. This header does not grant scopes; it only narrows authority. When `deviceAutoApprove.enabled` is true, the cap also limits the persistent device grant written by [automatic device approval](#automatic-device-approval).
Implications:
- Pairing is no longer the primary gate for device-less Control UI access. When `deviceAutoApprove.enabled` is true, the proxy identity also becomes the approval gate for new browser device enrollment.
- Pairing is no longer the primary gate for device-less Control UI access. A matching `identityScopes` entry can authorize that session without creating a pairing record. When `deviceAutoApprove.enabled` is true, the proxy identity also becomes the approval gate for new browser device enrollment.
- Your reverse proxy auth policy and `allowUsers` become the effective access control.
- Keep gateway ingress locked to trusted proxy IPs only (`gateway.trustedProxies` + firewall).
@@ -497,7 +534,7 @@ Separate, non-trusted-proxy-specific findings also apply whenever Control UI is
Common causes:
- Device-less Control UI session: trusted-proxy auth can admit the WebSocket connection without device identity, but OpenClaw clears scopes on device-less sessions by design.
- Device-less Control UI session: OpenClaw clears self-declared scopes by design, and no matching `gateway.auth.identityScopes` grant was configured.
- Custom backend client: the retired Control UI upgrade input never grants access to arbitrary backend or CLI-shaped WebSocket clients.
- Overly narrow `x-openclaw-scopes`: if your proxy injects this header on the Control UI WebSocket upgrade request, the session scopes are capped to that set. An empty header value yields no scopes.
@@ -247,6 +247,15 @@ beforeAll(async () => {
beforeEach(async () => {
vi.clearAllMocks();
state.runAgentAttemptMock.mockReset();
state.loadManifestModelCatalogMock.mockReset();
state.normalizeProviderModelIdWithRuntimeMock.mockReset();
state.runCliTurnCompactionLifecycleMock.mockReset();
state.deliverAgentCommandResultMock.mockReset();
state.emitAgentEventMock.mockReset();
state.persistCliTurnTranscriptMock.mockReset();
state.appendExactAssistantMessageMock.mockReset();
state.persistSessionEntryMock.mockReset();
state.loadManifestModelCatalogMock.mockReturnValue([]);
state.normalizeProviderModelIdWithRuntimeMock.mockImplementation(() => undefined);
state.runCliTurnCompactionLifecycleMock.mockImplementation(
@@ -474,11 +483,24 @@ describe("assistant transcript repair", () => {
);
await agentCommand({ message: "user one", sessionId, sessionKey, cwd: state.workspaceDir });
state.persistSessionEntryMock.mockRejectedValueOnce(new Error("simulated cleanup failure"));
let cleanupFailureInjected = false;
state.persistSessionEntryMock.mockImplementation(async (...args) => {
const [params] = args;
if (
!cleanupFailureInjected &&
params.initialEntry.pendingTranscriptRepair?.length &&
params.entry.pendingTranscriptRepair === undefined
) {
cleanupFailureInjected = true;
throw new Error("simulated cleanup failure");
}
return state.persistSessionEntryReal?.(...args);
});
state.runAgentAttemptMock.mockResolvedValueOnce(
makeResult({ sessionId, text: "assistant two", runner: "cli" }),
);
await agentCommand({ message: "user two", sessionId, sessionKey, cwd: state.workspaceDir });
expect(cleanupFailureInjected).toBe(true);
expect(findStoredSessionEntry(sessionKey)?.pendingTranscriptRepair).toHaveLength(1);
state.runAgentAttemptMock.mockResolvedValueOnce(
+2
View File
@@ -107,6 +107,8 @@ export const CORE_FIELD_HELP: Record<string, string> = {
'Gateway auth mode: "none", "token", "password", or "trusted-proxy" depending on your edge architecture. Use token/password for direct exposure, and trusted-proxy only behind hardened identity-aware proxies.',
"gateway.auth.allowTailscale":
"Allows trusted Tailscale identity paths to satisfy gateway auth checks when configured. Use this only when your tailnet identity posture is strong and operator workflows depend on it.",
"gateway.auth.identityScopes":
"Maps verified trusted-proxy or Tailscale identities to connection-only operator scope grants. Email keys match case-insensitively; grants augment device scopes before the connection scope cap is applied.",
"gateway.auth.rateLimit":
"Login/auth attempt throttling controls to reduce credential brute-force risk at the gateway boundary. Keep enabled in exposed environments and tune thresholds to your traffic baseline.",
"gateway.auth.trustedProxy":
+1
View File
@@ -118,6 +118,7 @@ export const FIELD_LABELS: Record<string, string> = {
"gateway.auth": "Gateway Auth",
"gateway.auth.mode": "Gateway Auth Mode",
"gateway.auth.allowTailscale": "Gateway Auth Allow Tailscale Identity",
"gateway.auth.identityScopes": "Gateway Identity Scope Grants",
"gateway.auth.rateLimit": "Gateway Auth Rate Limit",
"gateway.auth.trustedProxy": "Gateway Trusted Proxy Auth",
"gateway.auth.trustedProxy.deviceAutoApprove": "Trusted Proxy Device Auto-Approval",
+3
View File
@@ -1,4 +1,5 @@
// Defines gateway runtime and networking configuration types.
import type { OperatorScope } from "../gateway/operator-scopes.js";
import type { SecretInput } from "./types.secrets.js";
/** Gateway bind-address policy for local server startup. */
@@ -225,6 +226,8 @@ export type GatewayAuthConfig = {
password?: SecretInput;
/** Allow Tailscale identity headers when serve mode is enabled. */
allowTailscale?: boolean;
/** Operator scopes granted to verified trusted-proxy or Tailscale identities. */
identityScopes?: Record<string, OperatorScope[]>;
/** Rate-limit configuration for failed authentication attempts. */
rateLimit?: GatewayAuthRateLimitConfig;
/**
@@ -43,3 +43,22 @@ describe("gateway trusted-proxy device auto-approval config", () => {
},
);
});
describe("gateway identity scope grants config", () => {
test.each([
{ scope: "operator.admin", success: true },
{ scope: "operator.superuser", success: false },
])("validates configured scope $scope", ({ scope, success }) => {
const result = OpenClawSchema.safeParse({
gateway: {
auth: {
identityScopes: {
"admin@example.com": [scope],
},
},
},
});
expect(result.success).toBe(success);
});
});
+22
View File
@@ -1,4 +1,14 @@
import { z } from "zod";
import {
ADMIN_SCOPE,
APPROVALS_SCOPE,
PAIRING_SCOPE,
QUESTIONS_SCOPE,
READ_SCOPE,
TALK_SCOPE,
TALK_SECRETS_SCOPE,
WRITE_SCOPE,
} from "../gateway/operator-scopes.js";
import { SecretInputSchema } from "./zod-schema.core.js";
import {
GatewayRemoteConfigSchema,
@@ -7,6 +17,17 @@ import {
} from "./zod-schema.root-support.js";
import { sensitive } from "./zod-schema.sensitive.js";
const OperatorScopeSchema = z.enum([
ADMIN_SCOPE,
READ_SCOPE,
WRITE_SCOPE,
APPROVALS_SCOPE,
QUESTIONS_SCOPE,
PAIRING_SCOPE,
TALK_SCOPE,
TALK_SECRETS_SCOPE,
]);
export const GatewayConfigSchema = z
.strictObject({
port: z.number().int().min(1).max(65_535).optional(),
@@ -63,6 +84,7 @@ export const GatewayConfigSchema = z
token: SecretInputSchema.optional().register(sensitive),
password: SecretInputSchema.optional().register(sensitive),
allowTailscale: z.boolean().optional(),
identityScopes: z.record(z.string().min(1), z.array(OperatorScopeSchema)).optional(),
rateLimit: z
.strictObject({
maxAttempts: z.number().optional(),
@@ -0,0 +1,323 @@
import { randomUUID } from "node:crypto";
import os from "node:os";
import path from "node:path";
import { describe, expect, test } from "vitest";
import { writeConfigFile } from "../config/config.js";
import type { GatewayAuthConfig } from "../config/types.gateway.js";
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
import { getPairedDevice, listDevicePairing } from "../infra/device-pairing.js";
import {
connectReq,
CONTROL_UI_CLIENT,
installGatewayTestHooks,
NODE_CLIENT,
openTailscaleWs,
openWs,
rpcReq,
testState,
testTailscaleWhois,
withGatewayServer,
} from "./server.auth.test-helpers.js";
installGatewayTestHooks({ scope: "suite" });
const BROWSER_ORIGIN = "https://control.example.com";
const TRUSTED_PROXY_HEADERS = {
origin: BROWSER_ORIGIN,
"x-forwarded-for": "203.0.113.50",
"x-forwarded-proto": "https",
"x-forwarded-user": "admin@example.com",
};
function deviceIdentityPath(label: string): string {
return path.join(os.tmpdir(), `openclaw-${label}-${randomUUID()}.sqlite`);
}
async function configureGatewayAuth(auth: GatewayAuthConfig): Promise<void> {
testState.gatewayAuth = auth;
testState.gatewayControlUi = { allowedOrigins: [BROWSER_ORIGIN] };
await writeConfigFile({
gateway: {
auth,
trustedProxies: ["127.0.0.1"],
controlUi: { allowedOrigins: [BROWSER_ORIGIN] },
},
});
}
function responseScopes(response: Awaited<ReturnType<typeof connectReq>>): string[] | undefined {
return (response.payload as { auth?: { scopes?: string[] } } | undefined)?.auth?.scopes;
}
describe("gateway identity scope grants", () => {
test("adds a case-insensitive trusted-proxy email grant without changing pairing", async () => {
await configureGatewayAuth({
mode: "trusted-proxy",
identityScopes: { "admin@example.com": ["operator.admin"] },
trustedProxy: {
userHeader: "x-forwarded-user",
requiredHeaders: ["x-forwarded-proto"],
allowLoopback: true,
},
});
const identityPath = deviceIdentityPath("identity-scope-device");
const identity = loadOrCreateDeviceIdentity({ path: identityPath });
await withGatewayServer(async ({ port }) => {
const ws = await openWs(port, {
...TRUSTED_PROXY_HEADERS,
"x-forwarded-user": "Admin@Example.com",
});
try {
const connected = await connectReq(ws, {
skipDefaultAuth: true,
prePairDevice: true,
scopes: ["operator.read"],
client: CONTROL_UI_CLIENT,
deviceIdentityPath: identityPath,
browserOrigin: BROWSER_ORIGIN,
});
expect(connected.ok).toBe(true);
expect(responseScopes(connected)).toEqual(["operator.read", "operator.admin"]);
expect((await rpcReq(ws, "set-heartbeats", { enabled: false })).ok).toBe(true);
} finally {
ws.close();
}
});
expect((await getPairedDevice(identity.deviceId))?.approvedScopes).toEqual(["operator.read"]);
expect(
(await listDevicePairing()).pending.filter((entry) => entry.deviceId === identity.deviceId),
).toEqual([]);
});
test("applies a trusted-proxy grant after clearing device-less declared scopes", async () => {
await configureGatewayAuth({
mode: "trusted-proxy",
identityScopes: { "admin@example.com": ["operator.admin"] },
trustedProxy: {
userHeader: "x-forwarded-user",
requiredHeaders: ["x-forwarded-proto"],
allowLoopback: true,
},
});
await withGatewayServer(async ({ port }) => {
const ws = await openWs(port, TRUSTED_PROXY_HEADERS);
try {
const connected = await connectReq(ws, {
skipDefaultAuth: true,
scopes: ["operator.read"],
device: null,
client: CONTROL_UI_CLIENT,
});
expect(connected.ok).toBe(true);
expect(responseScopes(connected)).toEqual(["operator.admin"]);
} finally {
ws.close();
}
});
});
test.each([
{ configuredIdentity: "peter", verifiedIdentity: "peter", expectedAdmin: true },
{ configuredIdentity: "Peter", verifiedIdentity: "peter", expectedAdmin: false },
])(
"matches a verified Tailscale identity exactly ($configuredIdentity)",
async ({ configuredIdentity, verifiedIdentity, expectedAdmin }) => {
await configureGatewayAuth({
mode: "token",
token: "secret",
allowTailscale: true,
identityScopes: { [configuredIdentity]: ["operator.admin"] },
});
testTailscaleWhois.value = { login: verifiedIdentity, name: "Peter" };
await withGatewayServer(async ({ port }) => {
const ws = await openTailscaleWs(port, {
origin: BROWSER_ORIGIN,
"tailscale-user-login": verifiedIdentity,
});
try {
const connected = await connectReq(ws, {
skipDefaultAuth: true,
prePairDevice: true,
scopes: ["operator.read"],
client: CONTROL_UI_CLIENT,
deviceIdentityPath: deviceIdentityPath("identity-scope-tailscale"),
browserOrigin: BROWSER_ORIGIN,
});
expect(connected.ok).toBe(true);
expect(responseScopes(connected)).toEqual(
expectedAdmin ? ["operator.read", "operator.admin"] : ["operator.read"],
);
} finally {
ws.close();
}
});
},
);
test("caps the device and identity scope union", async () => {
await configureGatewayAuth({
mode: "trusted-proxy",
identityScopes: {
"admin@example.com": ["operator.admin", "operator.read"],
},
trustedProxy: {
userHeader: "x-forwarded-user",
requiredHeaders: ["x-forwarded-proto"],
allowLoopback: true,
},
});
await withGatewayServer(async ({ port }) => {
const ws = await openWs(port, {
...TRUSTED_PROXY_HEADERS,
"x-openclaw-scopes": "operator.read",
});
try {
const connected = await connectReq(ws, {
skipDefaultAuth: true,
prePairDevice: true,
scopes: ["operator.read"],
client: CONTROL_UI_CLIENT,
deviceIdentityPath: deviceIdentityPath("identity-scope-cap"),
browserOrigin: BROWSER_ORIGIN,
});
expect(connected.ok).toBe(true);
expect(responseScopes(connected)).toEqual(["operator.read"]);
expect((await rpcReq(ws, "status")).ok).toBe(true);
expect((await rpcReq(ws, "set-heartbeats", { enabled: false })).ok).toBe(false);
} finally {
ws.close();
}
});
});
test("caps a broader reconnect before device scope-upgrade comparison", async () => {
await configureGatewayAuth({
mode: "trusted-proxy",
identityScopes: { "admin@example.com": ["operator.admin"] },
trustedProxy: {
userHeader: "x-forwarded-user",
requiredHeaders: ["x-forwarded-proto"],
allowLoopback: true,
},
});
const identityPath = deviceIdentityPath("identity-scope-reconnect-cap");
const identity = loadOrCreateDeviceIdentity({ path: identityPath });
await withGatewayServer(async ({ port }) => {
const initialWs = await openWs(port, TRUSTED_PROXY_HEADERS);
try {
const initial = await connectReq(initialWs, {
skipDefaultAuth: true,
prePairDevice: true,
scopes: ["operator.read"],
client: CONTROL_UI_CLIENT,
deviceIdentityPath: identityPath,
browserOrigin: BROWSER_ORIGIN,
});
expect(initial.ok).toBe(true);
} finally {
initialWs.close();
}
const reconnectWs = await openWs(port, {
...TRUSTED_PROXY_HEADERS,
"x-openclaw-scopes": "operator.read",
});
try {
const reconnect = await connectReq(reconnectWs, {
skipDefaultAuth: true,
prePairDevice: false,
scopes: ["operator.read", "operator.write"],
client: CONTROL_UI_CLIENT,
deviceIdentityPath: identityPath,
browserOrigin: BROWSER_ORIGIN,
});
expect(reconnect.ok).toBe(true);
expect(responseScopes(reconnect)).toEqual(["operator.read"]);
} finally {
reconnectWs.close();
}
});
expect((await getPairedDevice(identity.deviceId))?.approvedScopes).toEqual(["operator.read"]);
expect(
(await listDevicePairing()).pending.filter((entry) => entry.deviceId === identity.deviceId),
).toEqual([]);
});
test.each([
{
name: "token",
auth: { mode: "token", token: "secret" } satisfies GatewayAuthConfig,
connectAuth: { token: "secret" },
},
{
name: "password",
auth: { mode: "password", password: "secret" } satisfies GatewayAuthConfig,
connectAuth: { password: "secret" },
},
{
name: "no auth",
auth: { mode: "none" } satisfies GatewayAuthConfig,
connectAuth: { skipDefaultAuth: true },
},
])("does not trust an identity header with $name", async ({ auth, connectAuth }) => {
await configureGatewayAuth({
...auth,
identityScopes: { "admin@example.com": ["operator.admin"] },
});
await withGatewayServer(async ({ port }) => {
const ws = await openWs(port, TRUSTED_PROXY_HEADERS);
try {
const connected = await connectReq(ws, {
...connectAuth,
prePairDevice: true,
scopes: ["operator.read"],
client: CONTROL_UI_CLIENT,
deviceIdentityPath: deviceIdentityPath(`identity-scope-${auth.mode}`),
browserOrigin: BROWSER_ORIGIN,
});
expect(connected.ok).toBe(true);
expect(responseScopes(connected)).toEqual(["operator.read"]);
} finally {
ws.close();
}
});
});
test("does not grant operator scopes to node connections", async () => {
await configureGatewayAuth({
mode: "trusted-proxy",
identityScopes: { "admin@example.com": ["operator.admin"] },
trustedProxy: {
userHeader: "x-forwarded-user",
requiredHeaders: ["x-forwarded-proto"],
allowLoopback: true,
},
});
await withGatewayServer(async ({ port }) => {
const ws = await openWs(port, TRUSTED_PROXY_HEADERS);
try {
const connected = await connectReq(ws, {
skipDefaultAuth: true,
prePairDevice: true,
role: "node",
scopes: [],
client: NODE_CLIENT,
deviceIdentityPath: deviceIdentityPath("identity-scope-node"),
});
expect(connected.ok).toBe(true);
expect(responseScopes(connected)).toEqual([]);
} finally {
ws.close();
}
});
});
});
@@ -20,11 +20,13 @@ import {
GATEWAY_STARTUP_PENDING_CLOSE_CAUSE,
GATEWAY_STARTUP_RETRY_AFTER_MS,
} from "../../../../packages/gateway-protocol/src/startup-unavailable.js";
import { roleScopesAllow } from "../../../shared/operator-scope-compat.js";
import {
isBrowserCopilotClient,
isBrowserOperatorUiClient,
isOperatorUiClient,
} from "../../../utils/message-channel.js";
import type { OperatorScope } from "../../operator-scopes.js";
import { checkBrowserOrigin, normalizeChromeExtensionOrigin } from "../../origin-check.js";
import { parseGatewayRole } from "../../role-policy.js";
import { formatForLog } from "../../ws-log.js";
@@ -32,14 +34,14 @@ import { truncateCloseReason } from "../close-reason.js";
import { isNativeAppUiClient } from "./handshake-auth-helpers.js";
import type { GatewayConnectPhaseContext } from "./message-handler-types.js";
export function resolveTrustedProxyControlUiScopes(params: {
requestedScopes: string[];
export function applyConnectionScopeCap(params: {
scopes: string[];
upgradeReq: IncomingMessage;
}): string[] {
const header = params.upgradeReq.headers["x-openclaw-scopes"];
const rawHeader = Array.isArray(header) ? header[0] : header;
if (rawHeader === undefined) {
return params.requestedScopes;
return params.scopes;
}
const declaredScopes = new Set(
rawHeader
@@ -49,7 +51,43 @@ export function resolveTrustedProxyControlUiScopes(params: {
);
return declaredScopes.size === 0
? []
: params.requestedScopes.filter((scope) => declaredScopes.has(scope));
: params.scopes.filter((scope) => declaredScopes.has(scope));
}
export function resolveEffectiveConnectionScopes(params: {
role: string;
deviceScopes: string[];
verifiedIdentity?: string;
identityScopes?: Record<string, OperatorScope[]>;
upgradeReq: IncomingMessage;
}): { scopes: string[]; addedIdentityScopes: OperatorScope[] } {
const verifiedIdentity = params.verifiedIdentity;
let identityScopes: OperatorScope[] = [];
if (params.role === "operator" && verifiedIdentity) {
const exactIdentityScopes = params.identityScopes?.[verifiedIdentity];
identityScopes = exactIdentityScopes ?? [];
if (exactIdentityScopes === undefined && verifiedIdentity.includes("@")) {
const normalizedIdentity = verifiedIdentity.toLowerCase();
identityScopes =
Object.entries(params.identityScopes ?? {}).find(
([identity]) => identity.includes("@") && identity.toLowerCase() === normalizedIdentity,
)?.[1] ?? [];
}
}
const scopes = applyConnectionScopeCap({
scopes: [...new Set([...params.deviceScopes, ...identityScopes])],
upgradeReq: params.upgradeReq,
});
const addedIdentityScopes = identityScopes.filter(
(scope) =>
scopes.includes(scope) &&
!roleScopesAllow({
role: "operator",
requestedScopes: [scope],
allowedScopes: params.deviceScopes,
}),
);
return { scopes, addedIdentityScopes };
}
export async function admitGatewayConnect(context: GatewayConnectPhaseContext) {
@@ -16,7 +16,7 @@ import { truncateCloseReason } from "../close-reason.js";
import { resolveSharedGatewaySessionGeneration } from "../ws-shared-generation.js";
import { resolveConnectAuthDecision, resolveConnectAuthState } from "./auth-context.js";
import { formatGatewayAuthFailureMessage } from "./auth-messages.js";
import { admitGatewayConnect, resolveTrustedProxyControlUiScopes } from "./connect-admission.js";
import { admitGatewayConnect, applyConnectionScopeCap } from "./connect-admission.js";
import { emitGatewayAuthSecurityEvent } from "./connect-auth-security.js";
import { isControlUiOperatorBootstrapProfile } from "./connect-device-metadata.js";
import { verifyGatewayConnectDeviceProof } from "./connect-device-proof.js";
@@ -461,10 +461,7 @@ export async function authenticateGatewayConnect(
authMethod,
});
if (trustedProxyAuthOk) {
scopes = resolveTrustedProxyControlUiScopes({
requestedScopes: scopes,
upgradeReq,
});
scopes = applyConnectionScopeCap({ scopes, upgradeReq });
connectParams.scopes = scopes;
}
const skipControlUiPairingForDevice = shouldSkipControlUiPairing(
@@ -34,7 +34,7 @@ import { shouldAutoApproveNodePairingFromTrustedCidrs } from "../../node-pairing
import { normalizeChromeExtensionOrigin } from "../../origin-check.js";
import { formatForLog } from "../../ws-log.js";
import { truncateCloseReason } from "../close-reason.js";
import { resolveTrustedProxyControlUiScopes } from "./connect-admission.js";
import { applyConnectionScopeCap } from "./connect-admission.js";
import {
isControlUiOwnerBootstrapProfile,
isControlUiOperatorBootstrapProfile,
@@ -398,8 +398,8 @@ export async function authorizeGatewayConnectDevice(
const trustedProxyAutoApproveScopes =
allowTrustedProxyDeviceAutoApproval &&
(pairing.request.isRepair !== true || isTrustedProxySameKeyUpgrade)
? resolveTrustedProxyControlUiScopes({
requestedScopes: resolveTrustedProxyDeviceAutoApproveScopes({
? applyConnectionScopeCap({
scopes: resolveTrustedProxyDeviceAutoApproveScopes({
requestedScopes: scopes,
hasRequestedScopes,
configuredScopes: trustedProxyAutoApproveConfig?.scopes,
@@ -50,6 +50,7 @@ import { truncateCloseReason } from "../close-reason.js";
import { incrementPresenceVersion } from "../health-state.js";
import { broadcastPresenceSnapshot } from "../presence-events.js";
import type { GatewayWsClient } from "../ws-types.js";
import { resolveEffectiveConnectionScopes } from "./connect-admission.js";
import { sendGatewayHello } from "./connect-hello.js";
import { prepareGatewayNodeConnect } from "./connect-node-session.js";
import type {
@@ -115,7 +116,7 @@ export async function attachAuthenticatedGatewayConnect(
maxProtocol,
usesLegacyNodeProtocol,
role,
scopes,
scopes: deviceScopes,
device,
devicePublicKey,
deviceToken,
@@ -186,6 +187,23 @@ export async function attachAuthenticatedGatewayConnect(
authResult.tailscaleIdentity &&
classifyTailscaleLogin(authResult.tailscaleIdentity.login).kind === "provider",
);
// Device pairing owns persistent access. Verified identity grants only shape
// this connection, after device-less self-declared scopes have been cleared.
const effectiveScopes = resolveEffectiveConnectionScopes({
role,
deviceScopes,
verifiedIdentity: authenticatedUserId,
identityScopes: context.configSnapshot.gateway?.auth?.identityScopes,
upgradeReq: context.handler.upgradeReq,
});
const scopes = effectiveScopes.scopes;
state.scopes = scopes;
connectParams.scopes = scopes;
if (authenticatedUserId && effectiveScopes.addedIdentityScopes.length > 0) {
logGateway.warn(
`security audit: identity scope grant elevated connection identity=${formatForLog(authenticatedUserId)} addedScopes=${effectiveScopes.addedIdentityScopes.join(",")} conn=${connId}`,
);
}
if (isClosed()) {
await releasePendingNodePairingCleanup();