mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 19:08:22 -06:00
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:
committed by
GitHub
parent
e97c3450a2
commit
c6edd3e80f
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"core": 2288,
|
||||
"channel": 3718,
|
||||
"plugin": 4053
|
||||
"core": 2291,
|
||||
"channel": 3716,
|
||||
"plugin": 4030
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user