@@ -10,6 +10,10 @@ import { applyPluginNodeInvokePolicy } from "./node-invoke-plugin-policy.js";
1010import type { NodeSession } from "./node-registry.js" ;
1111import type { GatewayClient , GatewayRequestContext } from "./server-methods/types.js" ;
1212
13+ const DEMO_PLUGIN_ID = "demo" ;
14+ const DEMO_COMMAND = "demo.read" ;
15+ const DEMO_PARAMS = { path : "/tmp/x" } ;
16+
1317const registryState = vi . hoisted ( ( ) => ( {
1418 current : null as PluginRegistry | null ,
1519} ) ) ;
@@ -94,35 +98,102 @@ function createOperatorClient(): GatewayClient {
9498 } ) ;
9599}
96100
101+ type NodeInvokePolicyRegistration = NonNullable < PluginRegistry [ "nodeInvokePolicies" ] > [ number ] ;
102+ type NodeInvokePolicyHandler = NodeInvokePolicyRegistration [ "policy" ] [ "handle" ] ;
103+ type PluginApprovalRecord = ReturnType <
104+ ExecApprovalManager < PluginApprovalRequestPayload > [ "listPendingRecords" ]
105+ > [ number ] ;
106+
107+ function createDemoPolicy ( handle : NodeInvokePolicyHandler ) : NodeInvokePolicyRegistration {
108+ return {
109+ pluginId : DEMO_PLUGIN_ID ,
110+ policy : {
111+ commands : [ DEMO_COMMAND ] ,
112+ handle,
113+ } ,
114+ pluginConfig : { enabled : true } ,
115+ source : "test" ,
116+ } ;
117+ }
118+
119+ function createApprovalRequestPolicy ( params ?: {
120+ timeoutMs ?: number ;
121+ } ) : NodeInvokePolicyRegistration {
122+ return createDemoPolicy ( async ( ctx : OpenClawPluginNodeInvokePolicyContext ) => {
123+ const approval = await ctx . approvals ?. request ( {
124+ title : "Sensitive action" ,
125+ description : "Needs approval" ,
126+ ...( params ?. timeoutMs === undefined ? { } : { timeoutMs : params . timeoutMs } ) ,
127+ } ) ;
128+ return { ok : true , payload : approval ?? null } ;
129+ } ) ;
130+ }
131+
132+ function setDangerousDemoCommandRegistry ( policies : NodeInvokePolicyRegistration [ ] = [ ] ) {
133+ registryState . current = {
134+ nodeHostCommands : [
135+ {
136+ pluginId : DEMO_PLUGIN_ID ,
137+ command : {
138+ command : DEMO_COMMAND ,
139+ dangerous : true ,
140+ handle : async ( ) => "{}" ,
141+ } ,
142+ source : "test" ,
143+ } ,
144+ ] ,
145+ nodeInvokePolicies : policies ,
146+ } as unknown as PluginRegistry ;
147+ }
148+
149+ async function invokeDemoPolicy (
150+ context : GatewayRequestContext ,
151+ client : GatewayClient | null = null ,
152+ ) {
153+ return await applyPluginNodeInvokePolicy ( {
154+ context,
155+ client,
156+ nodeSession : createNodeSession ( ) ,
157+ command : DEMO_COMMAND ,
158+ params : DEMO_PARAMS ,
159+ } ) ;
160+ }
161+
162+ async function expectSinglePendingApproval (
163+ manager : ExecApprovalManager < PluginApprovalRequestPayload > ,
164+ ) : Promise < PluginApprovalRecord > {
165+ await vi . waitFor ( ( ) => {
166+ expect ( manager . listPendingRecords ( ) ) . toHaveLength ( 1 ) ;
167+ } ) ;
168+ const [ record ] = manager . listPendingRecords ( ) ;
169+ if ( ! record ) {
170+ throw new Error ( "expected pending approval" ) ;
171+ }
172+ return record ;
173+ }
174+
175+ async function expectApprovalResolution (
176+ resultPromise : ReturnType < typeof applyPluginNodeInvokePolicy > ,
177+ manager : ExecApprovalManager < PluginApprovalRequestPayload > ,
178+ record : PluginApprovalRecord ,
179+ ) {
180+ expect ( manager . resolve ( record . id , "allow-once" ) ) . toBe ( true ) ;
181+ await expect ( resultPromise ) . resolves . toStrictEqual ( {
182+ ok : true ,
183+ payload : { id : record . id , decision : "allow-once" } ,
184+ } ) ;
185+ }
186+
97187describe ( "applyPluginNodeInvokePolicy" , ( ) => {
98188 beforeEach ( ( ) => {
99189 registryState . current = null ;
100190 } ) ;
101191
102192 it ( "fails closed for dangerous plugin node commands without a policy" , async ( ) => {
103- registryState . current = {
104- nodeHostCommands : [
105- {
106- pluginId : "demo" ,
107- command : {
108- command : "demo.read" ,
109- dangerous : true ,
110- handle : async ( ) => "{}" ,
111- } ,
112- source : "test" ,
113- } ,
114- ] ,
115- nodeInvokePolicies : [ ] ,
116- } as unknown as PluginRegistry ;
193+ setDangerousDemoCommandRegistry ( ) ;
117194 const { context, invoke } = createContext ( ) ;
118195
119- const result = await applyPluginNodeInvokePolicy ( {
120- context,
121- client : null ,
122- nodeSession : createNodeSession ( ) ,
123- command : "demo.read" ,
124- params : { path : "/tmp/x" } ,
125- } ) ;
196+ const result = await invokeDemoPolicy ( context ) ;
126197
127198 if ( result === null ) {
128199 throw new Error ( "expected plugin policy failure" ) ;
@@ -136,45 +207,18 @@ describe("applyPluginNodeInvokePolicy", () => {
136207 } ) ;
137208
138209 it ( "uses a matching plugin policy when one is registered" , async ( ) => {
139- registryState . current = {
140- nodeHostCommands : [
141- {
142- pluginId : "demo" ,
143- command : {
144- command : "demo.read" ,
145- dangerous : true ,
146- handle : async ( ) => "{}" ,
147- } ,
148- source : "test" ,
149- } ,
150- ] ,
151- nodeInvokePolicies : [
152- {
153- pluginId : "demo" ,
154- policy : {
155- commands : [ "demo.read" ] ,
156- handle : ( ctx : OpenClawPluginNodeInvokePolicyContext ) => ctx . invokeNode ( ) ,
157- } ,
158- pluginConfig : { enabled : true } ,
159- source : "test" ,
160- } ,
161- ] ,
162- } as unknown as PluginRegistry ;
210+ setDangerousDemoCommandRegistry ( [
211+ createDemoPolicy ( ( ctx : OpenClawPluginNodeInvokePolicyContext ) => ctx . invokeNode ( ) ) ,
212+ ] ) ;
163213 const { context, invoke } = createContext ( ) ;
164214
165- const result = await applyPluginNodeInvokePolicy ( {
166- context,
167- client : null ,
168- nodeSession : createNodeSession ( ) ,
169- command : "demo.read" ,
170- params : { path : "/tmp/x" } ,
171- } ) ;
215+ const result = await invokeDemoPolicy ( context ) ;
172216
173217 expect ( result ) . toStrictEqual ( { ok : true , payload : { ok : true , value : 1 } , payloadJSON : null } ) ;
174218 expect ( invoke ) . toHaveBeenCalledWith ( {
175219 nodeId : "node-1" ,
176- command : "demo.read" ,
177- params : { path : "/tmp/x" } ,
220+ command : DEMO_COMMAND ,
221+ params : DEMO_PARAMS ,
178222 timeoutMs : undefined ,
179223 idempotencyKey : undefined ,
180224 } ) ;
@@ -195,103 +239,33 @@ describe("applyPluginNodeInvokePolicy", () => {
195239 deviceId : "device-other" ,
196240 } ) ,
197241 ] ) ;
198- registryState . current = {
199- nodeHostCommands : [
200- {
201- pluginId : "demo" ,
202- command : {
203- command : "demo.read" ,
204- dangerous : true ,
205- handle : async ( ) => "{}" ,
206- } ,
207- source : "test" ,
208- } ,
209- ] ,
210- nodeInvokePolicies : [
211- {
212- pluginId : "demo" ,
213- policy : {
214- commands : [ "demo.read" ] ,
215- handle : async ( ctx : OpenClawPluginNodeInvokePolicyContext ) => {
216- const approval = await ctx . approvals ?. request ( {
217- title : "Sensitive action" ,
218- description : "Needs approval" ,
219- } ) ;
220- return { ok : true , payload : approval ?? null } ;
221- } ,
222- } ,
223- pluginConfig : { enabled : true } ,
224- source : "test" ,
225- } ,
226- ] ,
227- } as unknown as PluginRegistry ;
242+ setDangerousDemoCommandRegistry ( [ createApprovalRequestPolicy ( ) ] ) ;
228243 const { context } = createContext ( {
229244 pluginApprovalManager : manager ,
230245 getApprovalClientConnIds,
231246 } ) ;
232- const resultPromise = applyPluginNodeInvokePolicy ( {
233- context,
234- client : createOperatorClient ( ) ,
235- nodeSession : createNodeSession ( ) ,
236- command : "demo.read" ,
237- params : { path : "/tmp/x" } ,
238- } ) ;
247+ const resultPromise = invokeDemoPolicy ( context , createOperatorClient ( ) ) ;
239248
240- await vi . waitFor ( ( ) => {
241- expect ( manager . listPendingRecords ( ) ) . toHaveLength ( 1 ) ;
242- } ) ;
243- const [ record ] = manager . listPendingRecords ( ) ;
244- expect ( record ?. requestedByConnId ) . toBe ( "conn-requester" ) ;
245- expect ( record ?. requestedByDeviceId ) . toBe ( "device-owner" ) ;
246- expect ( record ?. requestedByClientId ) . toBe ( "client-owner" ) ;
249+ const record = await expectSinglePendingApproval ( manager ) ;
250+ expect ( record . requestedByConnId ) . toBe ( "conn-requester" ) ;
251+ expect ( record . requestedByDeviceId ) . toBe ( "device-owner" ) ;
252+ expect ( record . requestedByClientId ) . toBe ( "client-owner" ) ;
247253 expect ( context . broadcast ) . not . toHaveBeenCalled ( ) ;
248254 expect ( context . broadcastToConnIds ) . toHaveBeenCalledWith (
249255 "plugin.approval.requested" ,
250- expect . objectContaining ( { id : record ? .id } ) ,
256+ expect . objectContaining ( { id : record . id } ) ,
251257 visibleConnIds ,
252258 { dropIfSlow : true } ,
253259 ) ;
254260
255- expect ( manager . resolve ( record . id , "allow-once" ) ) . toBe ( true ) ;
256- await expect ( resultPromise ) . resolves . toStrictEqual ( {
257- ok : true ,
258- payload : { id : record ?. id , decision : "allow-once" } ,
259- } ) ;
261+ await expectApprovalResolution ( resultPromise , manager , record ) ;
260262 } ) ;
261263
262264 it ( "caps plugin policy approval timeouts through the shared approval policy" , async ( ) => {
263265 const manager = new ExecApprovalManager < PluginApprovalRequestPayload > ( ) ;
264- registryState . current = {
265- nodeHostCommands : [
266- {
267- pluginId : "demo" ,
268- command : {
269- command : "demo.read" ,
270- dangerous : true ,
271- handle : async ( ) => "{}" ,
272- } ,
273- source : "test" ,
274- } ,
275- ] ,
276- nodeInvokePolicies : [
277- {
278- pluginId : "demo" ,
279- policy : {
280- commands : [ "demo.read" ] ,
281- handle : async ( ctx : OpenClawPluginNodeInvokePolicyContext ) => {
282- const approval = await ctx . approvals ?. request ( {
283- title : "Sensitive action" ,
284- description : "Needs approval" ,
285- timeoutMs : Number . MAX_SAFE_INTEGER ,
286- } ) ;
287- return { ok : true , payload : approval ?? null } ;
288- } ,
289- } ,
290- pluginConfig : { enabled : true } ,
291- source : "test" ,
292- } ,
293- ] ,
294- } as unknown as PluginRegistry ;
266+ setDangerousDemoCommandRegistry ( [
267+ createApprovalRequestPolicy ( { timeoutMs : Number . MAX_SAFE_INTEGER } ) ,
268+ ] ) ;
295269 const { context } = createContext ( {
296270 pluginApprovalManager : manager ,
297271 getApprovalClientConnIds : createApprovalClientLookup ( [
@@ -302,25 +276,12 @@ describe("applyPluginNodeInvokePolicy", () => {
302276 } ) ,
303277 ] ) ,
304278 } ) ;
305- const resultPromise = applyPluginNodeInvokePolicy ( {
306- context,
307- client : createOperatorClient ( ) ,
308- nodeSession : createNodeSession ( ) ,
309- command : "demo.read" ,
310- params : { path : "/tmp/x" } ,
311- } ) ;
279+ const resultPromise = invokeDemoPolicy ( context , createOperatorClient ( ) ) ;
312280
313- await vi . waitFor ( ( ) => {
314- expect ( manager . listPendingRecords ( ) ) . toHaveLength ( 1 ) ;
315- } ) ;
316- const [ record ] = manager . listPendingRecords ( ) ;
281+ const record = await expectSinglePendingApproval ( manager ) ;
317282 expect ( record . expiresAtMs - record . createdAtMs ) . toBe ( MAX_PLUGIN_APPROVAL_TIMEOUT_MS ) ;
318283
319- expect ( manager . resolve ( record . id , "allow-once" ) ) . toBe ( true ) ;
320- await expect ( resultPromise ) . resolves . toStrictEqual ( {
321- ok : true ,
322- payload : { id : record . id , decision : "allow-once" } ,
323- } ) ;
284+ await expectApprovalResolution ( resultPromise , manager , record ) ;
324285 } ) ;
325286
326287 it ( "leaves commands without a dangerous plugin registration to normal allowlist handling" , async ( ) => {
0 commit comments