From 4b4b8c737c096bd84f81e826bf040ecda3a2e6b8 Mon Sep 17 00:00:00 2001 From: Chris Watson Date: Thu, 10 Mar 2022 12:36:01 +0000 Subject: [PATCH 01/61] adding docs folder for bridging --- docs/api-bridging/spec.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/api-bridging/spec.md diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md new file mode 100644 index 000000000..945c9b46d --- /dev/null +++ b/docs/api-bridging/spec.md @@ -0,0 +1 @@ +. \ No newline at end of file From 18d4f04e760f0e3f84375e8434a1321b85cecc31 Mon Sep 17 00:00:00 2001 From: Chris Watson <47246322+WatsonCIQ@users.noreply.github.com> Date: Thu, 10 Mar 2022 12:42:08 +0000 Subject: [PATCH 02/61] Notes from initial meeting as MD --- docs/api-bridging/spec.md | 197 +++++++++++++++++++++++++++++++++++++- 1 file changed, 196 insertions(+), 1 deletion(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 945c9b46d..78a9acf5e 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1 +1,196 @@ -. \ No newline at end of file +# Notes +Need to product some description of a protocol, to be used over a websocket, for exchanging messages about FDC3 calls between Desktop Agents. +## Overall protocol details +* Sender details to be added by websocket server to top level messages AND any embedded AppMetadata objects. + * AppMetadata needs a new `agent `field + * When a client connects to a server it should be assigned an identity of some sort, which can be used to augment messages with details of the agent + * The server should do the assignments and could generate ids or accept them via config. + * Clients don't need to know their own ids or even the ids of others, they just need to be able to pass around AppMetadata objects that contain them. +* Preserve message path as it passes through different servers? +* GUIDs required to uniquely identify messages + * To be referenced in replies +* Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls… + * for resilience, this may mean defining timeouts + * Desktop Agents may need GUIDs and / OR metadata - names? +## Individual message exchanges +### For broadcasts on channels +Only needs a single message (no response) +Somebody does `fdc3.broadcast(contextObj); `or` \ +(await fdc3.getOrCreateChannel(channelName)).broadcast(contextObj)` +``` +{ + guid: "", + timestamp: 2020-03-..., + type: "broadcast", + channel: channelName, + context: contxtObj +} +``` + \ +(server to add agent field) +### findIntent +``` +findIntent(intent: string, context?: Context): Promise; +``` +#### Request format: +``` +{ + requestGuid: string, + timestamp: date, + type: "findIntentRequest", + intent: string + context?: Context, + sourceAgent?: string //optional as filled in by server +} +``` +E.g. Call outward to other desktop agents (sent from A -> C \ + \ +`{` +``` + requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + timestamp: 2020-03-..., + type: "findIntentRequest", + intent: "ViewInstrument" + context?: contxtObj +} +``` +And repeated from C -> B as: +``` +{ + requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + timestamp: 2020-03-..., + type: "findIntentRequest", + intent: "ViewInstrument" + context?: contxtObj, + sourceAgent: "agent-A" +} +``` +#### Response format +``` +{ + requestGuid: string + responseGuid: string, + timestamp: Date, + type: "findIntentResponse", + intent: string + appIntent: AppIntent, + sourceAgent?: string, //optional, filled in by server + targetAgent: string +} +``` + \ +Server should augment the AppIntent.apps[AppMetadata] objects with the desktop agent as well as the sourceAgent field. targetAgent field should always be filled in with the sourceAgent of the request \ +E.g. +Normal response from:agent A (where the request was raised) - websocket client +``` +{ + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name: "myChat" } + ] +} +``` +Desktop agent B - websocket client \ +`{` +``` + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name: "Skype" }, + { name: "Symphony" }, + { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, + { name: "Slack" } + ] +} +``` +Desktop agent C - websocket server +``` +{ + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name "WebIce"} + ] +} +``` +Sent back over the bridge (by Agent B - which happens to be a websocket client) as: +``` +{ + requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + responseGuid: "b4cf1b91-0b64-45b6-9f55-65503d507024" + timestamp: 2020-03-..., + type: "findIntentResponse", + intent: "StartChat", + agent: undefined // can be undefined for server to fill in + appIntent: { + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name: "Skype"}, + { name: "Symphony" }, + { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, + { name: "Slack" } + ] + }, + targetAgent: "agent-A" +} +``` + \ +Which gets repeated by a server (agent-C) in augmented form as: +``` +{ + requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + responseGuid: "b4cf1b91-0b64-45b6-9f55-65503d507024" + timestamp: 2020-03-..., + type: "findIntentResponse", + intent: "StartChat", + appIntent: { + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name: "Skype", agent: "agent-B"}, + { name: "Symphony", agent: "agent-B" }, + { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859", agent: "agent-B" }, + { name: "Slack", agent: "agent-B" } + ] + }, + targetAgent: "agent-A" + sourceAgent: "agent-B" +} +``` +Agent-C also sends its own response: +``` +{ + requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + responseGuid: "988a49c8-49c2-4fb4-aad4-be39d1471834" + timestamp: 2020-03-..., + type: "findIntentResponse", + intent: "StartChat", + appIntent: { + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name "WebIce", agent: "agent-C"} + ] + }, + targetAgent: "agent-A" + sourceAgent: "agent-C" +} +``` +Then on agent-A the originating app finally gets back the following response from the FDC3 desktop agent C: +``` +{ + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name: "myChat" }, // local to this agent + { name: "Skype", agent: "agent-B" }, + { name: "Symphony", agent: "agent-B" }, + { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859", agent: "agent-B" }, + { name: "Slack", agent: "agent-B" }, + { name "WebIce", agent: "agent-C"} //this came from another DA + ] +} +``` +### raiseIntent +``` +raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; +``` +Note as IntentResolutions can now return a promise of result data there are multiple response formats required +#### Request format +#### Response format +#### Result Response format \ No newline at end of file From 48e5dd7576eb294518927027adc5714df938b9a5 Mon Sep 17 00:00:00 2001 From: Chris Watson <47246322+WatsonCIQ@users.noreply.github.com> Date: Thu, 10 Mar 2022 12:47:33 +0000 Subject: [PATCH 03/61] small formatting changes --- docs/api-bridging/spec.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 78a9acf5e..0621bd64d 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -15,7 +15,7 @@ Need to product some description of a protocol, to be used over a websocket, for ## Individual message exchanges ### For broadcasts on channels Only needs a single message (no response) -Somebody does `fdc3.broadcast(contextObj); `or` \ +Somebody does `fdc3.broadcast(contextObj); `or` (await fdc3.getOrCreateChannel(channelName)).broadcast(contextObj)` ``` { @@ -26,7 +26,7 @@ Somebody does `fdc3.broadcast(contextObj); `or` \ context: contxtObj } ``` - \ + (server to add agent field) ### findIntent ``` @@ -43,10 +43,9 @@ findIntent(intent: string, context?: Context): Promise; sourceAgent?: string //optional as filled in by server } ``` -E.g. Call outward to other desktop agents (sent from A -> C \ - \ -`{` +E.g. Call outward to other desktop agents (sent from A -> C ``` +{ requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", timestamp: 2020-03-..., type: "findIntentRequest", @@ -78,7 +77,7 @@ And repeated from C -> B as: targetAgent: string } ``` - \ + Server should augment the AppIntent.apps[AppMetadata] objects with the desktop agent as well as the sourceAgent field. targetAgent field should always be filled in with the sourceAgent of the request \ E.g. Normal response from:agent A (where the request was raised) - websocket client @@ -90,9 +89,10 @@ Normal response from:agent A (where the request was raised) - websocket client ] } ``` -Desktop agent B - websocket client \ -`{` +Desktop agent B - websocket client + ``` +{ intent: { name: "StartChat", displayName: "Chat" }, apps: [ { name: "Skype" }, @@ -132,7 +132,7 @@ Sent back over the bridge (by Agent B - which happens to be a websocket client) targetAgent: "agent-A" } ``` - \ + Which gets repeated by a server (agent-C) in augmented form as: ``` { From 264e1231d7b10ad2257496629aafbaf9151789cf Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 10 Mar 2022 14:01:58 +0000 Subject: [PATCH 04/61] separating out generic message formats an improving on the findIntent write up a bit --- docs/api-bridging/spec.md | 131 ++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 54 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 0621bd64d..1a5551034 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -12,17 +12,52 @@ Need to product some description of a protocol, to be used over a websocket, for * Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls… * for resilience, this may mean defining timeouts * Desktop Agents may need GUIDs and / OR metadata - names? + + +## Generic request and response formats + +### Request: +```typescript +{ + requestGuid: string, + timestamp: date, + type: string, //FDC3 function name message relates to, e.g. "findIntent" + intent: string + context?: Context, + sourceAgent?: string //optional as filled in by server +} +``` + +### Response: +Responses will be differentiated by the presence of a `responseGuid` field +```typescript +{ + requestGuid: string, //value from request + responseGuid: string, + timestamp: Date, + type: string, //same as request value + intent: string + appIntent: AppIntent, + sourceAgent?: string, //optional, filled in by server + targetAgent: string //sourceAgent from request +} +``` + + ## Individual message exchanges + +Assume that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. agent-C provides a websocket server that agent-A and agent-B have connected to. + ### For broadcasts on channels Only needs a single message (no response) -Somebody does `fdc3.broadcast(contextObj); `or` -(await fdc3.getOrCreateChannel(channelName)).broadcast(contextObj)` +An app on agent-A does `fdc3.broadcast(contextObj); `or` +(await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj)` ``` { - guid: "", + requestGuid: "some-guid-string-here", timestamp: 2020-03-..., type: "broadcast", - channel: channelName, + channel: "myChannel", context: contxtObj } ``` @@ -33,54 +68,38 @@ Somebody does `fdc3.broadcast(contextObj); `or` findIntent(intent: string, context?: Context): Promise; ``` #### Request format: -``` -{ - requestGuid: string, - timestamp: date, - type: "findIntentRequest", - intent: string - context?: Context, - sourceAgent?: string //optional as filled in by server -} -``` -E.g. Call outward to other desktop agents (sent from A -> C + +A findIntent call is made on agent-A. It sends an outward message to the other desktop agents (sent from A -> C (which then sends -> B)): +let appIntent = await fdc3.findIntent(); + ``` { requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", timestamp: 2020-03-..., - type: "findIntentRequest", - intent: "ViewInstrument" + type: "findIntent", + intent: "StartChat" context?: contxtObj } ``` + And repeated from C -> B as: ``` { requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", timestamp: 2020-03-..., - type: "findIntentRequest", - intent: "ViewInstrument" + type: "findIntent", + intent: "StartChat" context?: contxtObj, sourceAgent: "agent-A" } ``` + +Note that the `sourceAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. + #### Response format -``` -{ - requestGuid: string - responseGuid: string, - timestamp: Date, - type: "findIntentResponse", - intent: string - appIntent: AppIntent, - sourceAgent?: string, //optional, filled in by server - targetAgent: string -} -``` - -Server should augment the AppIntent.apps[AppMetadata] objects with the desktop agent as well as the sourceAgent field. targetAgent field should always be filled in with the sourceAgent of the request \ +When processing responses, the agent acting as the 'server' should augment and `AppMetadata` objects in responses with the desktop agent. targetAgent field should always be filled in with the sourceAgent of the request \ E.g. -Normal response from:agent A (where the request was raised) - websocket client +Normal response from:agent A,where the request was raised (a websocket client) ``` { intent: { name: "StartChat", displayName: "Chat" }, @@ -89,7 +108,7 @@ Normal response from:agent A (where the request was raised) - websocket client ] } ``` -Desktop agent B - websocket client +Desktop agent B (a websocket client) ``` { @@ -102,22 +121,14 @@ Desktop agent B - websocket client ] } ``` -Desktop agent C - websocket server -``` -{ - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ - { name "WebIce"} - ] -} -``` -Sent back over the bridge (by Agent B - which happens to be a websocket client) as: + +which is sent back over the bridge by Agent B -> C as: ``` { requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", responseGuid: "b4cf1b91-0b64-45b6-9f55-65503d507024" timestamp: 2020-03-..., - type: "findIntentResponse", + type: "findIntent", intent: "StartChat", agent: undefined // can be undefined for server to fill in appIntent: { @@ -133,13 +144,13 @@ Sent back over the bridge (by Agent B - which happens to be a websocket client) } ``` -Which gets repeated by a server (agent-C) in augmented form as: +Which gets repeated by the websocket server (agent-C) in augmented form as: ``` { requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", responseGuid: "b4cf1b91-0b64-45b6-9f55-65503d507024" timestamp: 2020-03-..., - type: "findIntentResponse", + type: "findIntent", intent: "StartChat", appIntent: { intent: { name: "StartChat", displayName: "Chat" }, @@ -149,18 +160,29 @@ Which gets repeated by a server (agent-C) in augmented form as: { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859", agent: "agent-B" }, { name: "Slack", agent: "agent-B" } ] - }, - targetAgent: "agent-A" - sourceAgent: "agent-B" + }, + targetAgent: "agent-A" + sourceAgent: "agent-B" } + + +Desktop agent C (the websocket server) as sends its own response: ``` -Agent-C also sends its own response: +{ + intent: { name: "StartChat", displayName: "Chat" }, + apps: [ + { name "WebIce"} + ] +} +``` + +which it encodes as a message: ``` { requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", responseGuid: "988a49c8-49c2-4fb4-aad4-be39d1471834" timestamp: 2020-03-..., - type: "findIntentResponse", + type: "findIntent", intent: "StartChat", appIntent: { intent: { name: "StartChat", displayName: "Chat" }, @@ -172,7 +194,7 @@ Agent-C also sends its own response: sourceAgent: "agent-C" } ``` -Then on agent-A the originating app finally gets back the following response from the FDC3 desktop agent C: +Then on agent-A the originating app finally gets back the following response from the FDC3 desktop agent: ``` { intent: { name: "StartChat", displayName: "Chat" }, @@ -186,6 +208,7 @@ Then on agent-A the originating app finally gets back the following response fro ] } ``` + ### raiseIntent ``` raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; From 0e5c3a5ecfc8593688bcd7ec941ce43254be1401 Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 10 Mar 2022 14:10:22 +0000 Subject: [PATCH 05/61] further tweaks --- docs/api-bridging/spec.md | 50 +++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 1a5551034..2534be542 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -22,26 +22,29 @@ Need to product some description of a protocol, to be used over a websocket, for requestGuid: string, timestamp: date, type: string, //FDC3 function name message relates to, e.g. "findIntent" - intent: string + intent: string, context?: Context, - sourceAgent?: string //optional as filled in by server + sourceAgent?: string //optional as filled in by server and subsequently used to route responses } ``` ### Response: -Responses will be differentiated by the presence of a `responseGuid` field +Responses will be differentiated by the presence of a `responseGuid` field. ```typescript { requestGuid: string, //value from request responseGuid: string, timestamp: Date, type: string, //same as request value - intent: string + intent: string, appIntent: AppIntent, - sourceAgent?: string, //optional, filled in by server - targetAgent: string //sourceAgent from request + sourceAgent?: string, //optionalsd filled in by server on receipt of message + targetAgent: string //sourceAgent from request, used to route response } ``` +Clients should send these messages on to the 'server', which will add the `sourceAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to sourceAgent. + + ## Individual message exchanges @@ -50,32 +53,53 @@ Assume that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. ### For broadcasts on channels Only needs a single message (no response) -An app on agent-A does `fdc3.broadcast(contextObj); `or` -(await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj)` +An app on agent-A does: +```javascript +fdc3.broadcast(contextObj); +``` +or +```javascript +(await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` + +It encodes this as a message which it sends to the websocker (hosted by agent-C): + +```JSON { requestGuid: "some-guid-string-here", - timestamp: 2020-03-..., + timestamp: "2020-03-...", type: "broadcast", channel: "myChannel", context: contxtObj } ``` -(server to add agent field) +which it repeats on to Agent-B with the sourceAgent metadata added: +```JSON +{ + requestGuid: "some-guid-string-here", + timestamp: "2020-03-...", + type: "broadcast", + channel: "myChannel", + context: contxtObj, + sourceAgent: "agent-A" +} +``` + + ### findIntent ``` findIntent(intent: string, context?: Context): Promise; ``` #### Request format: -A findIntent call is made on agent-A. It sends an outward message to the other desktop agents (sent from A -> C (which then sends -> B)): +A findIntent call is made on agent-A. It sends an outward message to the other desktop agents (sent from A -> C, which C then sends on to B)): let appIntent = await fdc3.findIntent(); ``` { requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - timestamp: 2020-03-..., + timestamp: "2020-03-...", type: "findIntent", intent: "StartChat" context?: contxtObj @@ -97,7 +121,7 @@ And repeated from C -> B as: Note that the `sourceAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. #### Response format -When processing responses, the agent acting as the 'server' should augment and `AppMetadata` objects in responses with the desktop agent. targetAgent field should always be filled in with the sourceAgent of the request \ + E.g. Normal response from:agent A,where the request was raised (a websocket client) ``` From 8ca8b0829a05a66fb7d3a073f5823fa1ce273242 Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 10 Mar 2022 14:55:23 +0000 Subject: [PATCH 06/61] Moving type specific message fields under a body element + correct JSON formatting --- docs/api-bridging/spec.md | 271 +++++++++++++++++++++----------------- 1 file changed, 152 insertions(+), 119 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 2534be542..204a86bb7 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -19,11 +19,19 @@ Need to product some description of a protocol, to be used over a websocket, for ### Request: ```typescript { + /** Unique guid for this request */ requestGuid: string, - timestamp: date, - type: string, //FDC3 function name message relates to, e.g. "findIntent" - intent: string, - context?: Context, + /** Timestamp at which request was generated */ + timestamp: date, + /** FDC3 function name message relates to, e.g. "findIntent" */ + type: string, + /** Request body, containing the arguments to the function called.*/ + body: { + channel?: string, + intent?: string + context?: Context, + //fields for other possible arguments + }, sourceAgent?: string //optional as filled in by server and subsequently used to route responses } ``` @@ -32,21 +40,29 @@ Need to product some description of a protocol, to be used over a websocket, for Responses will be differentiated by the presence of a `responseGuid` field. ```typescript { - requestGuid: string, //value from request - responseGuid: string, - timestamp: Date, - type: string, //same as request value - intent: string, - appIntent: AppIntent, - sourceAgent?: string, //optionalsd filled in by server on receipt of message - targetAgent: string //sourceAgent from request, used to route response + /** Value from request*/ + requestGuid: string, + /** Unique guid for this response */ + responseGuid: string, + /** Timestamp at which request was generated */ + timestamp: Date, + /** FDC3 function name the original request related to, e.g. "findIntent" */ + type: string, //same as request value + /** Response body */ + body: { + intent?: string, + appIntent?: AppIntent, + //fields for other possible response values + }, + /** Agent response received from, filled in by server on receipt of message */ + sourceAgent?: string, + /** sourceAgent from request, used to route response */ + targetAgent: string } ``` Clients should send these messages on to the 'server', which will add the `sourceAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to sourceAgent. - - ## Individual message exchanges Assume that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. agent-C provides a websocket server that agent-A and agent-B have connected to. @@ -62,59 +78,72 @@ or (await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` -It encodes this as a message which it sends to the websocker (hosted by agent-C): +It encodes this as a message which it sends to the websocket server(hosted by agent-C): ```JSON { - requestGuid: "some-guid-string-here", - timestamp: "2020-03-...", - type: "broadcast", - channel: "myChannel", - context: contxtObj + "requestGuid": "some-guid-string-here", + "timestamp": "2020-03-...", + "type": "broadcast", + "body": { + "channel": "myChannel", + "context": { /*contxtObj*/ } + } + } ``` -which it repeats on to Agent-B with the sourceAgent metadata added: +which it repeats on to Agent-B with the `sourceAgent` metadata added: ```JSON { - requestGuid: "some-guid-string-here", - timestamp: "2020-03-...", - type: "broadcast", - channel: "myChannel", - context: contxtObj, - sourceAgent: "agent-A" -} + "requestGuid": "some-guid-string-here", + "timestamp": "2020-03-...", + "type": "broadcast", + "body": { + "channel": "myChannel", + "context": { /*contxtObj*/} + }, + "sourceAgent": "agent-A" + } ``` ### findIntent -``` +```typescript findIntent(intent: string, context?: Context): Promise; ``` + #### Request format: -A findIntent call is made on agent-A. It sends an outward message to the other desktop agents (sent from A -> C, which C then sends on to B)): +A findIntent call is made on agent-A. +```javascript let appIntent = await fdc3.findIntent(); - ``` + +It sends an outward message to the other desktop agents (sent from A -> C): +```JSON { - requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - timestamp: "2020-03-...", - type: "findIntent", - intent: "StartChat" - context?: contxtObj + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": "2020-03-...", + "type": "findIntent", + "body": { + "intent": "StartChat", + "context": {/*contxtObj*/} + } } ``` -And repeated from C -> B as: -``` +which is repeated from C -> B as: +```JSON { - requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - timestamp: 2020-03-..., - type: "findIntent", - intent: "StartChat" - context?: contxtObj, - sourceAgent: "agent-A" + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": 2020-03-..., + "type": "findIntent", + "body": { + "intent": "StartChat", + "context": {/*contxtObj*/}, + }, + "sourceAgent": "agent-A" } ``` @@ -122,113 +151,117 @@ Note that the `sourceAgent` field has been populated with the id of the agent th #### Response format -E.g. -Normal response from:agent A,where the request was raised (a websocket client) -``` +Normal response from:agent A, where the request was raised (a websocket client) +```JSON { - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ - { name: "myChat" } + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ + { "name": "myChat" } ] } ``` -Desktop agent B (a websocket client) -``` +Desktop agent B (a websocket client) woud produce response: +```JSON { - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ - { name: "Skype" }, - { name: "Symphony" }, - { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, - { name: "Slack" } + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ + { "name": "Skype" }, + { "name": "Symphony" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859" }, + { "name": "Slack" } ] } ``` -which is sent back over the bridge by Agent B -> C as: -``` +which is sent back over the bridge by Agent B -> C as a response to the request message as: +```JSON { - requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - responseGuid: "b4cf1b91-0b64-45b6-9f55-65503d507024" - timestamp: 2020-03-..., - type: "findIntent", - intent: "StartChat", - agent: undefined // can be undefined for server to fill in - appIntent: { - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ - { name: "Skype"}, - { name: "Symphony" }, - { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, - { name: "Slack" } - ] + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "responseGuid": "b4cf1b91-0b64-45b6-9f55-65503d507024", + "timestamp": 2020-03-..., + "type": "findIntent", + "body": { + "intent": "StartChat", + "appIntent": { + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ + { "name": "Skype"}, + { "name": "Symphony" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859" }, + { "name": "Slack" } + ] + } }, - targetAgent: "agent-A" + "targetAgent": "agent-A" } ``` Which gets repeated by the websocket server (agent-C) in augmented form as: -``` +```JSON { - requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - responseGuid: "b4cf1b91-0b64-45b6-9f55-65503d507024" - timestamp: 2020-03-..., - type: "findIntent", - intent: "StartChat", - appIntent: { - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ - { name: "Skype", agent: "agent-B"}, - { name: "Symphony", agent: "agent-B" }, - { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859", agent: "agent-B" }, - { name: "Slack", agent: "agent-B" } - ] + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "responseGuid": "b4cf1b91-0b64-45b6-9f55-65503d507024", + "timestamp": 2020-03-..., + "type": "findIntent", + "body": { + "intent": "StartChat", + "appIntent": { + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ + { "name": "Skype", "agent": "agent-B"}, + { "name": "Symphony", "agent": "agent-B" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "agent": "agent-B" }, + { "name": "Slack", "agent": "agent-B" } + ] + } }, - targetAgent: "agent-A" - sourceAgent: "agent-B" + "targetAgent": "agent-A", + "sourceAgent": "agent-B" } - +``` Desktop agent C (the websocket server) as sends its own response: -``` +```JSON { - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ { name "WebIce"} ] } ``` which it encodes as a message: -``` +```JSON { - requestGuid: "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - responseGuid: "988a49c8-49c2-4fb4-aad4-be39d1471834" - timestamp: 2020-03-..., - type: "findIntent", - intent: "StartChat", - appIntent: { - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ - { name "WebIce", agent: "agent-C"} - ] - }, - targetAgent: "agent-A" - sourceAgent: "agent-C" + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "responseGuid": "988a49c8-49c2-4fb4-aad4-be39d1471834", + "timestamp": 2020-03-..., + "type": "findIntent", + "body": { + "intent": "StartChat", + "appIntent": { + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ + { name "WebIce", "agent": "agent-C"} + ] + } + }, + "targetAgent": "agent-A", + "sourceAgent": "agent-C" } ``` -Then on agent-A the originating app finally gets back the following response from the FDC3 desktop agent: -``` +Then on agent-A the originating app finally gets back the following response from the FDC3 desktop "agent": +```JSON { - intent: { name: "StartChat", displayName: "Chat" }, - apps: [ - { name: "myChat" }, // local to this agent - { name: "Skype", agent: "agent-B" }, - { name: "Symphony", agent: "agent-B" }, - { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859", agent: "agent-B" }, - { name: "Slack", agent: "agent-B" }, - { name "WebIce", agent: "agent-C"} //this came from another DA + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ + { "name": "myChat" }, // local to this agent + { "name": "Skype", "agent": "agent-B" }, + { "name": "Symphony", "agent": "agent-B" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "agent": "agent-B" }, + { "name": "Slack", "agent": "agent-B" }, + { "name": "WebIce", "agent": "agent-C"} ] } ``` From 7fa6bcfbbf4fe20055a99f38a9f8f962cdd27828 Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 10 Mar 2022 14:58:44 +0000 Subject: [PATCH 07/61] minor typo fixes + comments --- docs/api-bridging/spec.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 204a86bb7..501a8c592 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -89,7 +89,6 @@ It encodes this as a message which it sends to the websocket server(hosted by ag "channel": "myChannel", "context": { /*contxtObj*/ } } - } ``` @@ -104,7 +103,7 @@ which it repeats on to Agent-B with the `sourceAgent` metadata added: "context": { /*contxtObj*/} }, "sourceAgent": "agent-A" - } +} ``` @@ -221,12 +220,12 @@ Which gets repeated by the websocket server (agent-C) in augmented form as: } ``` -Desktop agent C (the websocket server) as sends its own response: +Desktop agent C (the websocket server) also sends its own response: ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { name "WebIce"} + { "name": "WebIce"} ] } ``` @@ -243,7 +242,7 @@ which it encodes as a message: "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { name "WebIce", "agent": "agent-C"} + { "name": "WebIce", "agent": "agent-C"} ] } }, @@ -257,11 +256,11 @@ Then on agent-A the originating app finally gets back the following response fro "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ { "name": "myChat" }, // local to this agent - { "name": "Skype", "agent": "agent-B" }, + { "name": "Skype", "agent": "agent-B" }, //agent-B responses { "name": "Symphony", "agent": "agent-B" }, { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "agent": "agent-B" }, { "name": "Slack", "agent": "agent-B" }, - { "name": "WebIce", "agent": "agent-C"} + { "name": "WebIce", "agent": "agent-C"} //agent C response ] } ``` From 529373bd492db66521a527a4b93f8197cf9cc2ad Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 10 Mar 2022 15:15:36 +0000 Subject: [PATCH 08/61] comment on request format --- docs/api-bridging/spec.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 501a8c592..799cdd145 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -32,7 +32,8 @@ Need to product some description of a protocol, to be used over a websocket, for context?: Context, //fields for other possible arguments }, - sourceAgent?: string //optional as filled in by server and subsequently used to route responses + /** Agent request received from, filled in by server on receipt of message */ + sourceAgent?: string } ``` From e59fa114ab9b81c56c960487c7b435f7413d70b1 Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 10 Mar 2022 15:16:41 +0000 Subject: [PATCH 09/61] comments --- docs/api-bridging/spec.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 799cdd145..13846ad8b 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -27,6 +27,7 @@ Need to product some description of a protocol, to be used over a websocket, for type: string, /** Request body, containing the arguments to the function called.*/ body: { + //example fields for specific messages... wouldn't be specified in base type channel?: string, intent?: string context?: Context, @@ -51,6 +52,7 @@ Responses will be differentiated by the presence of a `responseGuid` field. type: string, //same as request value /** Response body */ body: { + //example fields for specific messages... wouldn't be specified in base type intent?: string, appIntent?: AppIntent, //fields for other possible response values From 130d6b264363395b8d9f1ee57f5ca0ba73c39b7a Mon Sep 17 00:00:00 2001 From: Kris West Date: Thu, 10 Mar 2022 15:19:13 +0000 Subject: [PATCH 10/61] missing comma --- docs/api-bridging/spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 13846ad8b..5686511fc 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -29,7 +29,7 @@ Need to product some description of a protocol, to be used over a websocket, for body: { //example fields for specific messages... wouldn't be specified in base type channel?: string, - intent?: string + intent?: string, context?: Context, //fields for other possible arguments }, From 4586964b80ea93242cdb0774857caddcf13b7e98 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Fri, 11 Mar 2022 17:32:27 +0000 Subject: [PATCH 11/61] raiseIntent braindump --- docs/api-bridging/spec.md | 263 +++++++++++++++++++++++++++++++++++++- 1 file changed, 261 insertions(+), 2 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 5686511fc..98a32d143 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -253,7 +253,7 @@ which it encodes as a message: "sourceAgent": "agent-C" } ``` -Then on agent-A the originating app finally gets back the following response from the FDC3 desktop "agent": +Then on agent-A the originating app finally gets back the following response from the FDC3 desktop "agent-C": ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -269,10 +269,269 @@ Then on agent-A the originating app finally gets back the following response fro ``` ### raiseIntent -``` + +```typescript raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; ``` Note as IntentResolutions can now return a promise of result data there are multiple response formats required + #### Request format +A raiseIntent call is made on agent-A. + +```typescript +raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; +``` + +__WIP__ + +Just brain dumping stuff at the moment... + +``` +DA-A DA-B DA-C +| ---- raiseIntent ----> | | +| |---- raiseIntent --->| +| |<--- intentResolution ----| +|<-- resolutionOptions -----| | +|--- raiseIntent(... app)-->| | +| |------ raiseIntent ------->| +| |<--- intentResolution ----| +|<--- intentResolution -----| | +``` + + +Considerations: + +If no `TargetApp` is specified, then a `findIntent` needs to be sent to other Desktop Agents to get back options and send them to the desktop agent that the raiseIntent was called on. The `AppMetadata` returned by `findIntent` should contain the `agent` that the app lives on. + +If `TargetApp` is specified: ... + +??? what if we have AChartApp in agent-B and AchartApp in agent-C ??? + +???it is expected that the `AppMetadata` contains the target agent in that should resolve the intent.??? + + + +It sends an outward message to the other desktop agents (sent from A -> C): +```JSON +// no target +{ + "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", + "timestamp": "2020-03-...", + "type": "raiseIntent", + "body": { + "intent": "ViewChart", + "context": {/*contxtObj*/} + } +} +// with AppMetadata +{ + "requestGuid": "c916ee2e-feb7-437e-9ab3-be52ac46a6bc", + "timestamp": "2020-03-...", + "type": "raiseIntent", + "body": { + "intent": "ViewChart", + "context": {/*contxtObj*/}, + "app": { + "name": "AChartApp", + "agent": "agent-c" + } + } +} +// with AppName only +{ + "requestGuid": "2adbd9e5-5a22-4869-9a8c-95ca7ec8b6ae", + "timestamp": "2020-03-...", + "type": "raiseIntent", + "body": { + "intent": "ViewChart", + "context": {/*contxtObj*/}, + "app": "AChartApp" + } +} +``` + +Which is repeated from C -> B as: + +```JSON +{ + "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", + "timestamp": 2020-03-..., + "type": "raiseIntent", + "body": { + "intent": "ViewChart", + "context": {/*contxtObj*/}, + }, + "sourceAgent": "agent-A" +} +``` +Note that the sourceAgent field has been populated with the id of the agent that raised the requests, enabling the routing of responses. + #### Response format +Normal response from:agent A, where the request was raised (a websocket client) +```JSON +{ + "intent": "ViewChart", + "source": { + "name": "myChartA", + }, + "resolution": { + // with a context + "context": {/*contxtObj*/} + } +} +``` +or +```JSON +{ + "intent": "ViewChart", + "source": { + "name": "myChartA", + }, + "resolution": { + // with a channel + "id": "channel 1", + "type": "system" + } +} +``` + +Desktop agent B (a websocket client) woud produce response: +```JSON +{ + "intent": "ViewChart", + "source": { + "name": "myChartB", + }, + "resolution": { + "context": {/*contxtObj*/} + } +} +``` + +which is sent back over the bridge by Agent B -> C as a response to the request message as: +```JSON +{ + "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", + "responseGuid": "b8a86c63-049e-469d-bc76-bd1d10b17344", + "timestamp": 2020-03-..., + "type": "raiseIntent", + "body": { + "intentResolution": { + "intent": "ViewChart", + "source": { + "name": "myChartB", + }, + "resolution": { + "context": {/*contxtObj*/} + } + } + }, + "targetAgent": "agent-A" +} +``` + +Which gets repeated by the websocket server (agent-C) in augmented form as: +```JSON +{ + "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", + "responseGuid": "b8a86c63-049e-469d-bc76-bd1d10b17344", + "timestamp": 2020-03-..., + "type": "raiseIntent", + "body": { + "intentResolution": { + "intent": "ViewChart", + "source": { + "name": "myChartB", + }, + "resolution": { + "context": {/*contxtObj*/} // or channel + } + } + }, + "targetAgent": "agent-A", + "sourceAgent": "agent-B" +} +``` + +Desktop agent C (the websocket server) also sends its own response: +```JSON +{ + "intent": "ViewChart", + "source": { + "name": "myChartC", + }, + "resolution": { + "context": {/*contxtObj*/} + } +} +``` + +which it encodes as a message: +```JSON +{ + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "responseGuid": "1fe1a74f-2943-4ca4-b90a-afc317fd149c", + "timestamp": 2020-03-..., + "type": "raiseIntent", + "body": { + "intentResolution": { + "intent": "ViewChart", + "source": { + "name": "myChartC", + }, + "resolution": { + "context": {/*contxtObj*/} // or channel + } + } + }, + "targetAgent": "agent-A", + "sourceAgent": "agent-C" +} +``` + +Then agent C (ie. the server) should augment the responses so that a unequivocal resolution can be achieved (imagine 2 DA have app with the same name that are able to resolve the same intent) by agent-a. + +```JSON +// intent resolution response??? +{ + "intent": "ViewChart", + "sources": [ + "agent-A": { // agent-A intentResolution + "source": { + "name": "myChartA", + }, + }, + "agent-B": { // agent-B intentResolution + "source": { + "name": "myChartB", + }, + }, + "agent-c": { // agent-C intentResolution + "source": { + "name": "myChartC", + }, + }, + ], +} + +// another option +{ + "intent": "ViewChart", + "sources": [ + "source": { // agent-A intent resolver UI response + "name": "myChartA", + "agent": "agent-A" + }, + "source": { // agent-B intent resolver UI response + "name": "myChartB", + "agent": "agent-B" + }, + "source": { // agent-C intent resolver UI response + "name": "myChartC", + "agent": "agent-C" + }, + ], +} +``` + #### Result Response format \ No newline at end of file From c61cb842143075a36fce6f6c465f5df98070ef41 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Mon, 14 Mar 2022 10:57:41 +0000 Subject: [PATCH 12/61] update raiseIntent scenario --- docs/api-bridging/spec.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 98a32d143..af5128513 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -297,19 +297,35 @@ DA-A DA-B DA-C | |<--- intentResolution ----| |<--- intentResolution -----| | ``` +--- +**Assumptions & Questions** +- No client - client communication - All request go to server first which are then routed to the correct DA. +- The intentResolution result should be as is? The outcome of the raiseIntent should be the same as current? +**NOTE:** - (TP 14/03/2022) - The raiseIntent response should not really change (?) - Meaning, you raise and intent and the result of that would either be an intentResolution, that might need to contain information about which DA the intent was resolved, OR an error if the intent fails to resolve. -Considerations: +The raiseIntent request however, will need to be able to specify a target which must (should) include an DA as well. -If no `TargetApp` is specified, then a `findIntent` needs to be sent to other Desktop Agents to get back options and send them to the desktop agent that the raiseIntent was called on. The `AppMetadata` returned by `findIntent` should contain the `agent` that the app lives on. +Maybe we can strongly advise that `raiseIntent` should be preceeded by `findIntents`. -If `TargetApp` is specified: ... +No target specified +1. DA-A (client) sends `raiseIntent` request without target to DA-C (server) +2. DA-C MUST fire a `findIntent` to DA-A, DA-B and DA-C + * `findIntent` response only has one possible resolution + * No resolver UI is shown and a `findIntent` response is sent to DA-A which sends a `raiseIntent` with target. This should happen silently/transparently? + * `findIntent` response has multiple resolution possibilities + * Resolver UI is shown on DA-A which upon the user selecting the app will send a `raiseIntent` with target + * `findIntent` returns error because there is no possible resolution -??? what if we have AChartApp in agent-B and AchartApp in agent-C ??? -???it is expected that the `AppMetadata` contains the target agent in that should resolve the intent.??? +Target specified +1. DA-A (client) sends `raiseIntent` request with target (DA-B) to DA-C (server) +2. DA-C populates the sourceAgent in the raiseIntent request and forward the request to DA-B directly +3. DA-B sends `intentResolution` response to DA-C that fills in the targetAgent field +4. DA-C sends the augmented `intentResolution` response to DA-A +--- It sends an outward message to the other desktop agents (sent from A -> C): ```JSON @@ -534,4 +550,3 @@ Then agent C (ie. the server) should augment the responses so that a unequivocal } ``` -#### Result Response format \ No newline at end of file From 3e63310a1771f4eefa2d4c767740cdd6fcbcedf4 Mon Sep 17 00:00:00 2001 From: Chris Watson <47246322+WatsonCIQ@users.noreply.github.com> Date: Mon, 14 Mar 2022 23:55:15 +0000 Subject: [PATCH 13/61] Change header levels and add Apps section content. --- docs/api-bridging/spec.md | 123 +++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 7 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index af5128513..4b77535b1 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -67,16 +67,123 @@ Clients should send these messages on to the 'server', which will add the `sourc ## Individual message exchanges +The sections below cover all scenerios for each of the Desktop Agent methods. +Each section assumes that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. Agent-C provides a websocket server that agent-A and agent-B have connected to. -Assume that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. agent-C provides a websocket server that agent-A and agent-B have connected to. +## Apps +### Open +```typescript + open(app: TargetApp, context?: Context): Promise; +``` +#### Request format: +A findIntent call is made on agent-A. +```javascript +// Open an app without context, using the app name +let instanceMetadata = await fdc3.open('myApp'); + +// Open an app without context, using an AppMetadata object to specify the target +let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; +let instanceMetadata = await fdc3.open(appMetadata); + +// Open an app without context, using an AppMetadata object to specify the target and Desktop Agent +let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgents:["DesktopAgentB"]}; +let instanceMetadata = await fdc3.open(appMetadata); + +``` +There are three scenerios where Desktop Agent A sends an Open command +1) The app is opened on all Desktop Agents instances +2) The app is not found on any Desktop Agent +3) The Desktop Agent(s) that the app should open on is specified by the end user ahead of time +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Open Chart + DB ->>+ DC: Open Chart + DB ->> DB: App Found + DB ->> DB: Open App + DB -->>- DA: Return App Data + DC ->> DC: App Found + DC ->> DC: Open App + DC -->>- DB: Return App Data +``` +**When the desktop agent is in a list** +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Open App + DB --x DB: Is not in the desktopAgents list + DB ->>+ DC: Open App + DC ->> DC: Desktop agent in list and App Found + DC ->> DC: Open App + DC -->>- DB: Return App Data + DB -->>- DA:Return App Data +``` + +It sends an outward message to the other desktop agents (sent from A -> C): +```JSON +{ + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": "2020-03-...", + "type": "open", + "body": { + "appMetaData": { + "name": "myApp", + "appId": "myApp-v1.0.1", + "version": "1.0.1", + "desktopAgent":"DesktopAgentB" + }, + "context": {/*contxtObj*/} + } +} +``` +which is repeated from C -> B as: +```JSON +{ + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": 2020-03-..., + "type": "open", + "body": { + "appMetaData": { + "name": "myApp", + "appId": "myApp-v1.0.1", + "version": "1.0.1", + "desktopAgent":"DesktopAgentB" + }, + "context": {/*contxtObj*/} + }, + "sourceAgent": "agent-A" +} +``` + +### findInstances +```typescript + findInstances(app: TargetApp): Promise>; +``` + +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Find Instances of App + DB ->>+ DC: Find Instances of App + DC -->>- DB: Return App Data + DB -->>- DA:Return App Data +``` + +## Context ### For broadcasts on channels Only needs a single message (no response) An app on agent-A does: ```javascript fdc3.broadcast(contextObj); -``` -or +``` +or ```javascript (await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` @@ -109,7 +216,7 @@ which it repeats on to Agent-B with the `sourceAgent` metadata added: } ``` - +## Intents ### findIntent ```typescript findIntent(intent: string, context?: Context): Promise; @@ -117,7 +224,7 @@ findIntent(intent: string, context?: Context): Promise; #### Request format: -A findIntent call is made on agent-A. +A findIntent call is made on agent-A. ```javascript let appIntent = await fdc3.findIntent(); ``` @@ -323,7 +430,7 @@ Target specified 1. DA-A (client) sends `raiseIntent` request with target (DA-B) to DA-C (server) 2. DA-C populates the sourceAgent in the raiseIntent request and forward the request to DA-B directly 3. DA-B sends `intentResolution` response to DA-C that fills in the targetAgent field -4. DA-C sends the augmented `intentResolution` response to DA-A +4. DA-C sends the augmented `intentResolution` response to DA-A --- @@ -537,7 +644,7 @@ Then agent C (ie. the server) should augment the responses so that a unequivocal "source": { // agent-A intent resolver UI response "name": "myChartA", "agent": "agent-A" - }, + }, "source": { // agent-B intent resolver UI response "name": "myChartB", "agent": "agent-B" @@ -550,3 +657,5 @@ Then agent C (ie. the server) should augment the responses so that a unequivocal } ``` +## Channels + From 1e373ca1c9617de8a9eb07df5f69adb76e7898b7 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 22 Mar 2022 18:43:06 +0000 Subject: [PATCH 14/61] Meeting 20220315 (#14) * meeting draft * doc cleanup * Kris tweaks: includes a new preamble. Moved apps to the end and worked on fdc3.open logic Co-authored-by: Tiago Pina --- docs/api-bridging/spec.md | 1019 +++++++++++++++++++++++-------------- 1 file changed, 644 insertions(+), 375 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 4b77535b1..2c8cab027 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,44 +1,77 @@ -# Notes -Need to product some description of a protocol, to be used over a websocket, for exchanging messages about FDC3 calls between Desktop Agents. -## Overall protocol details +# Desktop Agent Bridging + +In order to implement Desktop Agent Bridging some means for Desktop Agents to communicate with each other is needed. One obvious solution is to support the use a websocket to communicate. However, to do so some idea of the protocol for that communication is needed. + +## Websocket protocol proposal +In a typical bridging scenario, one of more Desktop Agents will need to provide a websocket server for other agents to connect to - in the rest of this proposal the agent providing the websocket server will be referred to as 'the server' or 'a server', while Desktop Agents connecting to it are referred to as 'the client' or 'a client'. + +If a Desktop Agent acts as a server, this should not preclude it also being a client of another server (allowing for a variety of bridging topologies). However, there should exist only one connection between any two desktop agents. + +### Identifying Desktop Agents and Message Sources +In order to target intents and perform other actions that require specific routing between Desktop Agents, Desktop Agents need to have an identity. Identities should be assigned to clients when they connect to a server, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. + +To prevent spoofing and to simplify the implementation of clients, sender identities for birdging messages should be added, by the server to top level messages AND to AppMetadata objects embedded in them. + * Sender details to be added by websocket server to top level messages AND any embedded AppMetadata objects. - * AppMetadata needs a new `agent `field + * AppMetadata needs a new `desktopAgent `field * When a client connects to a server it should be assigned an identity of some sort, which can be used to augment messages with details of the agent * The server should do the assignments and could generate ids or accept them via config. * Clients don't need to know their own ids or even the ids of others, they just need to be able to pass around AppMetadata objects that contain them. -* Preserve message path as it passes through different servers? + +### Identifying Individual Messages +There are a variety of message types we'll need to send between bridged Desktop Agents, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. + * GUIDs required to uniquely identify messages * To be referenced in replies -* Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls… - * for resilience, this may mean defining timeouts - * Desktop Agents may need GUIDs and / OR metadata - names? + +### Handling FDC3 calls When Bridged +* Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls. + * for resilience, this may mean defining timeouts... + +### Forwarding of Messages from Other Agents +To enable support for a vairety of topologies, it is necessary for a Desktop Agent to be able to forward messages received from one Desktop Agent on to others. There are a few simple rules which determine whether a message needs to be forwarded: +- the message does not have a target Desktop Agent (e.g. findIntent) + - If you are a client of a server, send it on to the server + - If you are a server, send it on to your clients (except the source of the message) + - If you are both do both +- If the message has a target Destkop Agent (e.g. response to findIntent) + - If you are a server and the target is one of your clients forward the message to it. + - If you are a client (and teh target isn't you) forward the message to your server. -## Generic request and response formats +### Open Questions +* Is it necessary to preserve message path as it passes through different servers? -### Request: + +### Generic request and response formats + +#### Request: ```typescript { - /** Unique guid for this request */ - requestGuid: string, - /** Timestamp at which request was generated */ - timestamp: date, - /** FDC3 function name message relates to, e.g. "findIntent" */ - type: string, - /** Request body, containing the arguments to the function called.*/ - body: { - //example fields for specific messages... wouldn't be specified in base type - channel?: string, - intent?: string, - context?: Context, - //fields for other possible arguments - }, - /** Agent request received from, filled in by server on receipt of message */ - sourceAgent?: string + /** Unique guid for this request */ + requestGuid: string, + /** Timestamp at which request was generated */ + timestamp: date, + /** FDC3 function name message relates to, e.g. "findIntent" */ + type: string, + /** Request body, containing the arguments to the function called.*/ + payload: { + //example fields for specific messages... wouldn't be specified in base type + channel?: string, + intent?: string, + context?: Context, + //fields for other possible arguments + }, + /** AppMetadata source request received from */ + source?: { + AppMetadata & { + desktopAgent?: string // filled in by server on receipt of message + } + } } ``` -### Response: +#### Response: Responses will be differentiated by the presence of a `responseGuid` field. ```typescript { @@ -51,130 +84,35 @@ Responses will be differentiated by the presence of a `responseGuid` field. /** FDC3 function name the original request related to, e.g. "findIntent" */ type: string, //same as request value /** Response body */ - body: { + payload: { //example fields for specific messages... wouldn't be specified in base type intent?: string, appIntent?: AppIntent, //fields for other possible response values }, - /** Agent response received from, filled in by server on receipt of message */ - sourceAgent?: string, - /** sourceAgent from request, used to route response */ - targetAgent: string + /** AppMetadata source request received from */ + source?: { + AppMetadata & { + desktopAgent?: string // filled in by server on receipt of message + } + } + /** AppMetadata destination response sent from */ + destination?: { + AppMetadata & { + desktopAgent?: string // filled in by server on receipt of message + } + } } ``` -Clients should send these messages on to the 'server', which will add the `sourceAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to sourceAgent. - +Clients should send these messages on to the 'server', which will add the `source.desktopAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. -## Individual message exchanges -The sections below cover all scenerios for each of the Desktop Agent methods. -Each section assumes that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. Agent-C provides a websocket server that agent-A and agent-B have connected to. - -## Apps -### Open -```typescript - open(app: TargetApp, context?: Context): Promise; -``` -#### Request format: -A findIntent call is made on agent-A. -```javascript -// Open an app without context, using the app name -let instanceMetadata = await fdc3.open('myApp'); - -// Open an app without context, using an AppMetadata object to specify the target -let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; -let instanceMetadata = await fdc3.open(appMetadata); - -// Open an app without context, using an AppMetadata object to specify the target and Desktop Agent -let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgents:["DesktopAgentB"]}; -let instanceMetadata = await fdc3.open(appMetadata); - -``` -There are three scenerios where Desktop Agent A sends an Open command -1) The app is opened on all Desktop Agents instances -2) The app is not found on any Desktop Agent -3) The Desktop Agent(s) that the app should open on is specified by the end user ahead of time -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DB: Open Chart - DB ->>+ DC: Open Chart - DB ->> DB: App Found - DB ->> DB: Open App - DB -->>- DA: Return App Data - DC ->> DC: App Found - DC ->> DC: Open App - DC -->>- DB: Return App Data -``` -**When the desktop agent is in a list** -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DB: Open App - DB --x DB: Is not in the desktopAgents list - DB ->>+ DC: Open App - DC ->> DC: Desktop agent in list and App Found - DC ->> DC: Open App - DC -->>- DB: Return App Data - DB -->>- DA:Return App Data -``` -It sends an outward message to the other desktop agents (sent from A -> C): -```JSON -{ - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "timestamp": "2020-03-...", - "type": "open", - "body": { - "appMetaData": { - "name": "myApp", - "appId": "myApp-v1.0.1", - "version": "1.0.1", - "desktopAgent":"DesktopAgentB" - }, - "context": {/*contxtObj*/} - } -} -``` +### Individual message exchanges +The sections below cover most scenerios for each of the Desktop Agent methods in order to explore how this protocol might work. -which is repeated from C -> B as: -```JSON -{ - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "timestamp": 2020-03-..., - "type": "open", - "body": { - "appMetaData": { - "name": "myApp", - "appId": "myApp-v1.0.1", - "version": "1.0.1", - "desktopAgent":"DesktopAgentB" - }, - "context": {/*contxtObj*/} - }, - "sourceAgent": "agent-A" -} -``` +Each section assumes that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. Agent-C provides a websocket server that agent-A and agent-B have connected to. -### findInstances -```typescript - findInstances(app: TargetApp): Promise>; -``` -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DB: Find Instances of App - DB ->>+ DC: Find Instances of App - DC -->>- DB: Return App Data - DB -->>- DA:Return App Data -``` ## Context ### For broadcasts on channels @@ -188,34 +126,53 @@ or (await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` -It encodes this as a message which it sends to the websocket server(hosted by agent-C): +It encodes this as a message which it sends to the websocket server (agent-C) + +Message flow: agent-A -> agent-C ```JSON { "requestGuid": "some-guid-string-here", "timestamp": "2020-03-...", "type": "broadcast", - "body": { + "payload": { "channel": "myChannel", "context": { /*contxtObj*/ } + }, + "source": { + "name": "...", + "appId": "...", + "version": "...", + // ... other metadata fields } } ``` -which it repeats on to Agent-B with the `sourceAgent` metadata added: +which it repeats on to Agent-B with the `source.desktopAgent` metadata added. + +Message flow: agent-C -> agent-B + ```JSON { "requestGuid": "some-guid-string-here", "timestamp": "2020-03-...", "type": "broadcast", - "body": { + "payload": { "channel": "myChannel", "context": { /*contxtObj*/} }, - "sourceAgent": "agent-A" + "source": { + "desktopAgent": "agent-A", + "name": "...", + "appId": "...", + "version": "...", + // ... other metadata fields + } } ``` +When adding context listeners (either for User channels or specific App Channels) no messages need to be exchanged. Instead, upon receving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. + ## Intents ### findIntent ```typescript @@ -229,38 +186,53 @@ A findIntent call is made on agent-A. let appIntent = await fdc3.findIntent(); ``` -It sends an outward message to the other desktop agents (sent from A -> C): +Sends an outward message to the desktop agent(s) acting as server(s). + ```JSON +// agent-A -> agent-C { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "requestGuid": "requestGuid", "timestamp": "2020-03-...", "type": "findIntent", - "body": { + "payload": { "intent": "StartChat", "context": {/*contxtObj*/} - } + }, + "source": { + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + } } ``` -which is repeated from C -> B as: +The server (agent-C) fills in the `source.desktopAgent` field and forwards the request to the other desktop agents. + ```JSON +// agent-C -> agent-B { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "timestamp": 2020-03-..., + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", "type": "findIntent", - "body": { + "payload": { "intent": "StartChat", "context": {/*contxtObj*/}, }, - "sourceAgent": "agent-A" + "source": { + "desktoAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + } } ``` - -Note that the `sourceAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. +Note that the `source.desktopAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. #### Response format -Normal response from:agent A, where the request was raised (a websocket client) +Normal response from agent-A, where the request was raised (a websocket client) ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -283,14 +255,16 @@ Desktop agent B (a websocket client) woud produce response: } ``` -which is sent back over the bridge by Agent B -> C as a response to the request message as: +which is sent back over the bridge as a response to the request message as: + ```JSON +// agent-B -> agent-C { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "b4cf1b91-0b64-45b6-9f55-65503d507024", - "timestamp": 2020-03-..., - "type": "findIntent", - "body": { + "requestGuid": "requestGuid", + "responseGuid": "requestAgentBGuid", + "timestamp": "2020-03-...", + "type": "findIntentResponse", + "payload": { "intent": "StartChat", "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -302,31 +276,46 @@ which is sent back over the bridge by Agent B -> C as a response to the request ] } }, - "targetAgent": "agent-A" + "destination": { + "desktopAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + } } ``` Which gets repeated by the websocket server (agent-C) in augmented form as: + ```JSON { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "b4cf1b91-0b64-45b6-9f55-65503d507024", - "timestamp": 2020-03-..., - "type": "findIntent", - "body": { + "requestGuid": "requestGuid", + "responseGuid": "requestAgentB_Guid", + "timestamp": "2020-03-...", + "type": "findIntentResponse", + "payload": { "intent": "StartChat", "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "Skype", "agent": "agent-B"}, - { "name": "Symphony", "agent": "agent-B" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "agent": "agent-B" }, - { "name": "Slack", "agent": "agent-B" } + { "name": "Skype", "desktopAgent": "agent-B"}, + { "name": "Symphony", "desktopAgent": "agent-B" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, + { "name": "Slack", "desktopAgent": "agent-B" } ] } }, - "targetAgent": "agent-A", - "sourceAgent": "agent-B" + "destination": { + "desktopAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + }, + "source": { + "desktoAgent": "agent-B", + } } ``` @@ -343,34 +332,44 @@ Desktop agent C (the websocket server) also sends its own response: which it encodes as a message: ```JSON { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "988a49c8-49c2-4fb4-aad4-be39d1471834", - "timestamp": 2020-03-..., - "type": "findIntent", - "body": { + "requestGuid": "requestGuid", + "responseGuid": "requestAgentC_Guid", + "timestamp": "2020-03-...", + "type": "findIntentResponse", + "payload": { "intent": "StartChat", "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "WebIce", "agent": "agent-C"} + { "name": "WebIce", "desktopAgent": "agent-C"} ] } }, - "targetAgent": "agent-A", - "sourceAgent": "agent-C" + "destination": { + "desktopAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + }, + "source": { + "desktoAgent": "agent-C", + } } ``` -Then on agent-A the originating app finally gets back the following response from the FDC3 desktop "agent-C": +Then on agent-A the originating app finally gets back the following response from agent-C: + ```JSON +// agent-C -> agent-A { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ { "name": "myChat" }, // local to this agent - { "name": "Skype", "agent": "agent-B" }, //agent-B responses - { "name": "Symphony", "agent": "agent-B" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "agent": "agent-B" }, - { "name": "Slack", "agent": "agent-B" }, - { "name": "WebIce", "agent": "agent-C"} //agent C response + { "name": "Skype", "desktopAgent": "agent-B" }, //agent-B responses + { "name": "Symphony", "desktopAgent": "agent-B" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, + { "name": "Slack", "desktopAgent": "agent-B" }, + { "name": "WebIce", "desktopAgent": "agent-C"} //agent C response ] } ``` @@ -380,282 +379,552 @@ Then on agent-A the originating app finally gets back the following response fro ```typescript raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; ``` -Note as IntentResolutions can now return a promise of result data there are multiple response formats required +For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead. See details below. #### Request format -A raiseIntent call is made on agent-A. +A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. ```typescript -raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; +raiseIntent(intent: string, context: Context): Promise; ``` -__WIP__ - -Just brain dumping stuff at the moment... +agent-A sends an outward `findIntent` message to the desktop agent(s) acting as server(s): +```JSON +// agent-A -> agent-C +{ + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "findIntent", + "payload": { + "intent": "StartChat", + "context": {/*contxtObj*/} + }, + "source": { + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + } +} ``` -DA-A DA-B DA-C -| ---- raiseIntent ----> | | -| |---- raiseIntent --->| -| |<--- intentResolution ----| -|<-- resolutionOptions -----| | -|--- raiseIntent(... app)-->| | -| |------ raiseIntent ------->| -| |<--- intentResolution ----| -|<--- intentResolution -----| | -``` ---- -**Assumptions & Questions** -- No client - client communication - All request go to server first which are then routed to the correct DA. -- The intentResolution result should be as is? The outcome of the raiseIntent should be the same as current? - -**NOTE:** - (TP 14/03/2022) - The raiseIntent response should not really change (?) - Meaning, you raise and intent and the result of that would either be an intentResolution, that might need to contain information about which DA the intent was resolved, OR an error if the intent fails to resolve. -The raiseIntent request however, will need to be able to specify a target which must (should) include an DA as well. +This will trigger the same flow of `findIntent`. Upon receiveing a `findIntentResponse`, the resolver is shown. -Maybe we can strongly advise that `raiseIntent` should be preceeded by `findIntents`. +User selects an option which will trigger a `raiseIntent` call with a `app:TargetApp` argument. -No target specified -1. DA-A (client) sends `raiseIntent` request without target to DA-C (server) -2. DA-C MUST fire a `findIntent` to DA-A, DA-B and DA-C - * `findIntent` response only has one possible resolution - * No resolver UI is shown and a `findIntent` response is sent to DA-A which sends a `raiseIntent` with target. This should happen silently/transparently? - * `findIntent` response has multiple resolution possibilities - * Resolver UI is shown on DA-A which upon the user selecting the app will send a `raiseIntent` with target - * `findIntent` returns error because there is no possible resolution - - -Target specified +--- -1. DA-A (client) sends `raiseIntent` request with target (DA-B) to DA-C (server) -2. DA-C populates the sourceAgent in the raiseIntent request and forward the request to DA-B directly -3. DA-B sends `intentResolution` response to DA-C that fills in the targetAgent field -4. DA-C sends the augmented `intentResolution` response to DA-A +A `raiseIntent` call is made on agent-A which targets an `AChatApp` in agent-B. ---- +```typescript +raiseIntent(intent: string, context: Context, app: TargetApp): Promise; +``` -It sends an outward message to the other desktop agents (sent from A -> C): ```JSON -// no target +// agent-A -> agent-C { - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", + "requestGuid": "requestGuid", "timestamp": "2020-03-...", "type": "raiseIntent", - "body": { - "intent": "ViewChart", - "context": {/*contxtObj*/} + "payload": { + "intent": "StartChat", + "context": {/*contxtObj*/}, + "app": { + "name": "AChatApp", + "desktopAgent": "agent-B" + } + }, + "source": { + "name": "", + "appId": "", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "AChatApp", + "desktopAgent": "agent-B" + } } } -// with AppMetadata +``` + +The agent-C (server) fills in the `source.desktopAgent` field and forwards the request to the target desktop agent. + +```JSON +// agent-C -> agent-B { - "requestGuid": "c916ee2e-feb7-437e-9ab3-be52ac46a6bc", - "timestamp": "2020-03-...", - "type": "raiseIntent", - "body": { - "intent": "ViewChart", - "context": {/*contxtObj*/}, - "app": { - "name": "AChartApp", - "agent": "agent-c" + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "raiseIntent", + "payload": { + "intent": "StartChat", + "context": {/*contxtObj*/}, + }, + "source": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" // <---- filled by server (C) + // ... other metadata fields + }, + "destination": { + "app": { + "name": "AChatApp", + "desktopAgent": "agent-B" } } } -// with AppName only +``` + + +#### Response format +Normal response from agent-B (to-C), where the request was targeted to by agent-A. It sends this `intentResolution` as soon as it delivers the `raiseIntent` to the target app. + +```JSON +// agent-B -> agent-C { - "requestGuid": "2adbd9e5-5a22-4869-9a8c-95ca7ec8b6ae", - "timestamp": "2020-03-...", - "type": "raiseIntent", - "body": { - "intent": "ViewChart", - "context": {/*contxtObj*/}, - "app": "AChartApp" + "requestGuid": "requestGuid", + "responseGuid": "responseGuid", + "timestamp": "2020-03-...", + "type": "intentResolution", + "payload": { + "intent": "StartChat", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields + }, + "version": "...", + }, + "error?:": "ResolveError Enum", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } } } ``` -Which is repeated from C -> B as: +agent-C (server) will fill in the `source.DesktopAgent` and relay the message on to agent-A. ```JSON +// agent-C -> agent-A { - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", - "timestamp": 2020-03-..., - "type": "raiseIntent", - "body": { - "intent": "ViewChart", - "context": {/*contxtObj*/}, + "requestGuid": "requestGuid", + "responseGuid": "intentResolutionResponseGuid", + "timestamp": "2020-03-...", + "type": "intentResolution", + "payload": { + "intent": "StartChat", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields + }, + "version": "...", }, - "sourceAgent": "agent-A" + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } + } } ``` -Note that the sourceAgent field has been populated with the id of the agent that raised the requests, enabling the routing of responses. -#### Response format -Normal response from:agent A, where the request was raised (a websocket client) +When `AChatApp` produces a response, or the intent handler finishes running, it should send a further `intentResult` message to send that response onto the intent raiser (or throw an error if one occurred) + ```JSON +// agent-B -> agent-C -> agent-A { - "intent": "ViewChart", + "requestGuid": "requestGuid", + "responseGuid": "intentResultResponseGuid", + "timestamp": "2020-03-...", + "type": "intentResult", + "payload?:": { + "channel": { + "id": "channel 1", + "type": "system" + }, + "context": {/*contextObj*/} // in alternative to channel + }, + "error?:": "ResultError Enum", "source": { - "name": "myChartA", + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields }, - "resolution": { - // with a context - "context": {/*contxtObj*/} - } + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } + } } ``` -or + +If intent result is private channel: + ```JSON +// agent-B -> agent-C -> agent-A { - "intent": "ViewChart", + "requestGuid": "requestGuid", + "responseGuid": "intentResultResponseGuid", + "timestamp": "2020-03-...", + "type": "intentResult", + "payload?:": { + "channel": { + "id": "channel a", + "type": "private" + }, + "context": {/*contextObj*/} // in alternative to channel + }, + "error?:": "ResultError Enum", "source": { - "name": "myChartA", + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields }, - "resolution": { - // with a channel - "id": "channel 1", - "type": "system" - } + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } + } } ``` +--- +`onSubscribe` to the private channel sent to server: -Desktop agent B (a websocket client) woud produce response: ```JSON +// agent-A -> agent-C { - "intent": "ViewChart", + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "privateChannelSubscribe", + "payload": {}, "source": { - "name": "myChartB", + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields }, - "resolution": { - "context": {/*contxtObj*/} - } + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } } ``` -which is sent back over the bridge by Agent B -> C as a response to the request message as: +Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) + ```JSON +// agent-A -> agent-C { - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", - "responseGuid": "b8a86c63-049e-469d-bc76-bd1d10b17344", - "timestamp": 2020-03-..., - "type": "raiseIntent", - "body": { - "intentResolution": { - "intent": "ViewChart", - "source": { - "name": "myChartB", - }, - "resolution": { - "context": {/*contxtObj*/} - } - } + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "privateChannelSubscribe", + "payload": {}, + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields }, - "targetAgent": "agent-A" + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } } ``` +--- +`onUnsubscribe` to the private channel sent to server -Which gets repeated by the websocket server (agent-C) in augmented form as: ```JSON +// agent-A -> agent-C { - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", - "responseGuid": "b8a86c63-049e-469d-bc76-bd1d10b17344", - "timestamp": 2020-03-..., - "type": "raiseIntent", - "body": { - "intentResolution": { - "intent": "ViewChart", - "source": { - "name": "myChartB", - }, - "resolution": { - "context": {/*contxtObj*/} // or channel - } - } + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "privateChannelUnsubscribe", + "payload": {}, + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields }, - "targetAgent": "agent-A", - "sourceAgent": "agent-B" + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } } ``` -Desktop agent C (the websocket server) also sends its own response: +Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) + ```JSON +// agent-C -> agent-B { - "intent": "ViewChart", + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "privateChannelUnsubscribe", + "payload": {}, "source": { - "name": "myChartC", + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A", + // ... other metadata fields }, - "resolution": { - "context": {/*contxtObj*/} - } + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } } ``` +--- +`onDisconnect` to the private channel sent to server -which it encodes as a message: ```JSON +// agent-A -> agent-C { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "1fe1a74f-2943-4ca4-b90a-afc317fd149c", - "timestamp": 2020-03-..., - "type": "raiseIntent", - "body": { - "intentResolution": { - "intent": "ViewChart", - "source": { - "name": "myChartC", - }, - "resolution": { - "context": {/*contxtObj*/} // or channel - } - } + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "privateChannelDisconnect", + "payload": {}, + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields }, - "targetAgent": "agent-A", - "sourceAgent": "agent-C" + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } } ``` -Then agent C (ie. the server) should augment the responses so that a unequivocal resolution can be achieved (imagine 2 DA have app with the same name that are able to resolve the same intent) by agent-a. +Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// intent resolution response??? +// agent-C -> agent-B { - "intent": "ViewChart", - "sources": [ - "agent-A": { // agent-A intentResolution - "source": { - "name": "myChartA", - }, - }, - "agent-B": { // agent-B intentResolution - "source": { - "name": "myChartB", - }, - }, - "agent-c": { // agent-C intentResolution - "source": { - "name": "myChartC", - }, - }, - ], + "type": "privateChannelDisconnect", + "payload": {}, + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } +} +``` +--- + +### `fdc3.open` +```typescript + open(app: TargetApp, context?: Context): Promise; +``` +#### Request format: +A `fdc3.open`` call is made on agent-A. + +```javascript +// Open an app without context, using the app name +let instanceMetadata = await fdc3.open('myApp'); + +// Open an app without context, using an AppMetadata object to specify the target +let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; +let instanceMetadata = await fdc3.open(appMetadata); + +// Open an app without context, using an AppMetadata object to specify the target and Desktop Agent +let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgent:"DesktopAgentB"}; +let instanceMetadata = await fdc3.open(appMetadata); +``` + + +The `fdc3.open` command should result in a single copy of the specified app being opened and its instance data returned, or an error if it could not be opened. There are two possible scenarios: + +1) The Desktop Agent that the app should open on is specified +2) The Desktop Agent that the app should open on is NOT specified app + +The first case (target Desktop Agent is specified) is simple: +- If the local Desktop Agent is the target, handle the call normally +- If you are a server + - check if any of your clients is the target and transmit the call to them and await a response + - If you are also a client of another server follow the client steps + - otherwise return `OpenError.AppNotFound` +- If you are a client + - transmit the call to the server and await a response + +The second case is a little trickier as we don't know which agent may have the app available: +- If the local Desktop Agent has the app, open it and exit. +- If you are a server + - call each client one at a time and await a response + - If the response is `OpenError.AppNotFound` move to the next client + - If the response is AppMetadata then return it and exit + - If you are also a client of another server follow the client steps + - otherwise return `OpenError.AppNotFound` +- If you are a client + - transmit the call to the server and await a response + + + +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Open Chart + DB ->>+ DC: Open Chart + DB ->> DB: App Found + DB ->> DB: Open App + DB -->>- DA: Return App Data + DC ->> DC: App Found + DC ->> DC: Open App + DC -->>- DB: Return App Data +``` + +**When the target Desktop Agent is set** +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Open App + DB --x DB: Is not in the desktopAgents list + DB ->>+ DC: Open App + DC ->> DC: Desktop agent in list and App Found + DC ->> DC: Open App + DC -->>- DB: Return App Data + DB -->>- DA:Return App Data +``` + +It sends an outward message to the other desktop agents (sent from A -> C): +```JSON +{ + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": "2020-03-...", + "type": "open", + "payload": { + "appMetaData": { + "name": "myApp", + "appId": "myApp-v1.0.1", + "version": "1.0.1", + "desktopAgent":"agent-B" + }, + "context": {/*contxtObj*/} + } } +``` -// another option +which is repeated from C -> B as: +```JSON { - "intent": "ViewChart", - "sources": [ - "source": { // agent-A intent resolver UI response - "name": "myChartA", - "agent": "agent-A" - }, - "source": { // agent-B intent resolver UI response - "name": "myChartB", - "agent": "agent-B" - }, - "source": { // agent-C intent resolver UI response - "name": "myChartC", - "agent": "agent-C" - }, - ], + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": 2020-03-..., + "type": "open", + "payload": { + "appMetaData": { + "name": "myApp", + "appId": "myApp-v1.0.1", + "version": "1.0.1", + "desktopAgent":"DesktopAgentB" + }, + "context": {/*contxtObj*/} + }, + "sourceAgent": "agent-A" } ``` +### `fdc3.findInstances` +```typescript + findInstances(app: TargetApp): Promise>; +``` + + +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Find Instances of App + DB ->>+ DC: Find Instances of App + DC -->>- DB: Return App Data + DB -->>- DA: Return App Data +``` + ## Channels +App Channels don't need specific messages sending for `fdc3.getOrCreateChannel` as other agents will be come aware of it when messages are broadcast. However, `PrivateChannel` instances do require additional handling due to the listeners for subscription and disconnect. + From 2bde817c7ad1e4240bcfda028590726dca9e715c Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 23 Mar 2022 11:39:03 +0000 Subject: [PATCH 15/61] adjusting a number of notes in proposal write-up --- docs/api-bridging/spec.md | 44 ++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 2c8cab027..47ddf2733 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -379,7 +379,7 @@ Then on agent-A the originating app finally gets back the following response fro ```typescript raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; ``` -For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead. See details below. +For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead to collect options to display in a local resolver UI, allowing for a targetted intent to be raised afterwards. See details below. #### Request format A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. @@ -401,15 +401,15 @@ agent-A sends an outward `findIntent` message to the desktop agent(s) acting as "context": {/*contxtObj*/} }, "source": { - "name": "", - "appId": "", - "version": "", + "name": "someOtherApp", //should this be the desktop agent or the app? + "appId": "...", + "version": "...", // ... other metadata fields } } ``` -This will trigger the same flow of `findIntent`. Upon receiveing a `findIntentResponse`, the resolver is shown. +This will trigger the same flow as `findIntent`. Upon receiveing a `findIntentResponse`, the resolver is shown. User selects an option which will trigger a `raiseIntent` call with a `app:TargetApp` argument. @@ -436,11 +436,12 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise From 90d3168dc21697a82aa2d7a33da18d92c7af99ab Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 29 Mar 2022 18:03:34 +0100 Subject: [PATCH 16/61] Added paths Updated AppMetadata --- docs/api-bridging/spec.md | 1174 ++++++++++++++++++++++++------------- 1 file changed, 781 insertions(+), 393 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 4b77535b1..8a031d17c 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,180 +1,161 @@ -# Notes -Need to product some description of a protocol, to be used over a websocket, for exchanging messages about FDC3 calls between Desktop Agents. -## Overall protocol details +# Desktop Agent Bridging + +In order to implement Desktop Agent Bridging some means for Desktop Agents to communicate with each other is needed. One obvious solution is to support the use a websocket to communicate. However, to do so some idea of the protocol for that communication is needed. + +## Websocket protocol proposal +In a typical bridging scenario, one of more Desktop Agents will need to provide a websocket server for other agents to connect to - in the rest of this proposal the agent providing the websocket server will be referred to as 'the server' or 'a server', while Desktop Agents connecting to it are referred to as 'the client' or 'a client'. + +If a Desktop Agent acts as a server, this should not preclude it also being a client of another server (allowing for a variety of bridging topologies). However, there should exist only one connection between any two desktop agents. + +### Identifying Desktop Agents and Message Sources +In order to target intents and perform other actions that require specific routing between Desktop Agents, Desktop Agents need to have an identity. Identities should be assigned to clients when they connect to a server, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. + +To prevent spoofing and to simplify the implementation of clients, sender identities for birdging messages should be added, by the server to top level messages AND to AppMetadata objects embedded in them. + * Sender details to be added by websocket server to top level messages AND any embedded AppMetadata objects. - * AppMetadata needs a new `agent `field + * AppMetadata needs a new `desktopAgent `field * When a client connects to a server it should be assigned an identity of some sort, which can be used to augment messages with details of the agent * The server should do the assignments and could generate ids or accept them via config. * Clients don't need to know their own ids or even the ids of others, they just need to be able to pass around AppMetadata objects that contain them. -* Preserve message path as it passes through different servers? + +### Identifying Individual Messages +There are a variety of message types we'll need to send between bridged Desktop Agents, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. + * GUIDs required to uniquely identify messages * To be referenced in replies -* Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls… - * for resilience, this may mean defining timeouts - * Desktop Agents may need GUIDs and / OR metadata - names? + +### Handling FDC3 calls When Bridged +* Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls. + * for resilience, this may mean defining timeouts... + +### Forwarding of Messages from Other Agents +To enable support for a vairety of topologies, it is necessary for a Desktop Agent to be able to forward messages received from one Desktop Agent on to others. There are a few simple rules which determine whether a message needs to be forwarded: +- the message does not have a target Desktop Agent (e.g. findIntent) + - If you are a client of a server, send it on to the server + - If you are a server, send it on to your clients (except the source of the message) + - If you are both do both +- If the message has a target Destkop Agent (e.g. response to findIntent) + - If you are a server and the target is one of your clients forward the message to it. + - If you are a client (and teh target isn't you) forward the message to your server. + + +### Open Questions +* Is it necessary to preserve message path as it passes through different servers? -## Generic request and response formats +### AppMetadata +`AppMetadata` needs to be expanded to contain a `desktopAgent` field. -### Request: +```typescript +interface AppMetadata { + /** The unique app name that can be used with the open and raiseIntent calls. */ + readonly name: string; + + /** The unique application identifier located within a specific application directory instance. An example of an appId might be 'app@sub.root' */ + readonly appId?: string; + + /** The Version of the application. */ + readonly version?: string; + + /** An optional instance identifier, indicating that this object represents a specific instance of the application described.*/ + readonly instanceId?: string; + + /** An optional set of, implementation specific, metadata fields that can be used to disambiguate instances, such as a window title or screen position. Must only be set if `instanceId` is set. */ + readonly instanceMetadata?: Record; + + /** A more user-friendly application title that can be used to render UI elements */ + readonly title?: string; + + /** A tooltip for the application that can be used to render UI elements */ + readonly tooltip?: string; + + /** A longer, multi-paragraph description for the application that could include mark-up */ + readonly description?: string; + + /** A list of icon URLs for the application that can be used to render UI elements */ + readonly icons?: Array; + + /** A list of image URLs for the application that can be used to render UI elements */ + readonly images?: Array; + + /** The type of result returned for any intent specified during resolution. + * May express a particular context type (e.g. "fdc3.instrument"), channel + * (e.g. "channel") or a channel that will receive a specified type + * (e.g. "channel"). */ + readonly resultType?: string | null; + + /** A string filled in by server on receipt of message, that represents + * the Desktop Agent that the app is available on. */ + readonly desktopAgent?: string; +} +``` + +### Generic request and response formats + +#### Request: ```typescript { - /** Unique guid for this request */ - requestGuid: string, - /** Timestamp at which request was generated */ - timestamp: date, - /** FDC3 function name message relates to, e.g. "findIntent" */ - type: string, - /** Request body, containing the arguments to the function called.*/ - body: { - //example fields for specific messages... wouldn't be specified in base type - channel?: string, - intent?: string, - context?: Context, - //fields for other possible arguments - }, - /** Agent request received from, filled in by server on receipt of message */ - sourceAgent?: string + /** FDC3 function name message relates to, e.g. "findIntent" */ + type: string, + /** Request body, containing the arguments to the function called.*/ + payload: { + //example fields for specific messages... wouldn't be specified in base type + channel?: string, + intent?: string, + context?: Context, + //fields for other possible arguments + }, + meta: { + /** Unique guid for this request */ + requestGuid: string, + /** Timestamp at which request was generated */ + timestamp: date, + /** AppMetadata source request received from */ + source?: AppMetadata, + /** The path (Desktop Agents) the request went through */ + requestPath?: string[] + } } ``` -### Response: +#### Response: Responses will be differentiated by the presence of a `responseGuid` field. ```typescript { - /** Value from request*/ - requestGuid: string, - /** Unique guid for this response */ - responseGuid: string, - /** Timestamp at which request was generated */ - timestamp: Date, /** FDC3 function name the original request related to, e.g. "findIntent" */ - type: string, //same as request value + type: string, /** Response body */ - body: { + payload: { //example fields for specific messages... wouldn't be specified in base type intent?: string, appIntent?: AppIntent, //fields for other possible response values }, - /** Agent response received from, filled in by server on receipt of message */ - sourceAgent?: string, - /** sourceAgent from request, used to route response */ - targetAgent: string + meta: { + /** Value from request*/ + requestGuid: string, + /** Unique guid for this response */ + responseGuid: string, + /** Timestamp at which request was generated */ + timestamp: Date, + /** AppMetadata source request received from */ + source?: AppMetadata, + /** AppMetadata destination response sent from */ + destination?: AppMetadata, + /** The path (Desktop Agents) the responses came from */ + responsePaths?: string[][] + } } ``` -Clients should send these messages on to the 'server', which will add the `sourceAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to sourceAgent. - - -## Individual message exchanges -The sections below cover all scenerios for each of the Desktop Agent methods. -Each section assumes that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. Agent-C provides a websocket server that agent-A and agent-B have connected to. +Clients should send these messages on to the 'server', which will add the `source.desktopAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. -## Apps -### Open -```typescript - open(app: TargetApp, context?: Context): Promise; -``` -#### Request format: -A findIntent call is made on agent-A. -```javascript -// Open an app without context, using the app name -let instanceMetadata = await fdc3.open('myApp'); -// Open an app without context, using an AppMetadata object to specify the target -let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; -let instanceMetadata = await fdc3.open(appMetadata); - -// Open an app without context, using an AppMetadata object to specify the target and Desktop Agent -let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgents:["DesktopAgentB"]}; -let instanceMetadata = await fdc3.open(appMetadata); - -``` -There are three scenerios where Desktop Agent A sends an Open command -1) The app is opened on all Desktop Agents instances -2) The app is not found on any Desktop Agent -3) The Desktop Agent(s) that the app should open on is specified by the end user ahead of time -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DB: Open Chart - DB ->>+ DC: Open Chart - DB ->> DB: App Found - DB ->> DB: Open App - DB -->>- DA: Return App Data - DC ->> DC: App Found - DC ->> DC: Open App - DC -->>- DB: Return App Data -``` -**When the desktop agent is in a list** -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DB: Open App - DB --x DB: Is not in the desktopAgents list - DB ->>+ DC: Open App - DC ->> DC: Desktop agent in list and App Found - DC ->> DC: Open App - DC -->>- DB: Return App Data - DB -->>- DA:Return App Data -``` - -It sends an outward message to the other desktop agents (sent from A -> C): -```JSON -{ - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "timestamp": "2020-03-...", - "type": "open", - "body": { - "appMetaData": { - "name": "myApp", - "appId": "myApp-v1.0.1", - "version": "1.0.1", - "desktopAgent":"DesktopAgentB" - }, - "context": {/*contxtObj*/} - } -} -``` +### Individual message exchanges +The sections below cover most scenerios for each of the Desktop Agent methods in order to explore how this protocol might work. -which is repeated from C -> B as: -```JSON -{ - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "timestamp": 2020-03-..., - "type": "open", - "body": { - "appMetaData": { - "name": "myApp", - "appId": "myApp-v1.0.1", - "version": "1.0.1", - "desktopAgent":"DesktopAgentB" - }, - "context": {/*contxtObj*/} - }, - "sourceAgent": "agent-A" -} -``` +Each section assumes that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. Agent-C provides a websocket server that agent-A and agent-B have connected to. -### findInstances -```typescript - findInstances(app: TargetApp): Promise>; -``` -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DB: Find Instances of App - DB ->>+ DC: Find Instances of App - DC -->>- DB: Return App Data - DB -->>- DA:Return App Data -``` ## Context ### For broadcasts on channels @@ -188,34 +169,58 @@ or (await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` -It encodes this as a message which it sends to the websocket server(hosted by agent-C): +It encodes this as a message which it sends to the websocket server (agent-C) + +Message flow: agent-A -> agent-C ```JSON { - "requestGuid": "some-guid-string-here", - "timestamp": "2020-03-...", "type": "broadcast", - "body": { + "payload": { "channel": "myChannel", "context": { /*contxtObj*/ } + }, + "meta": { + "requestGuid": "some-guid-string-here", + "timestamp": "2020-03-...", + "source": { + "name": "...", + "appId": "...", + "version": "...", + // ... other metadata fields + } } } ``` -which it repeats on to Agent-B with the `sourceAgent` metadata added: +which it repeats on to Agent-B with the `source.desktopAgent` and `requestPath` array metadata added. + +Message flow: agent-C -> agent-B + ```JSON { - "requestGuid": "some-guid-string-here", - "timestamp": "2020-03-...", "type": "broadcast", - "body": { + "payload": { "channel": "myChannel", "context": { /*contxtObj*/} }, - "sourceAgent": "agent-A" + "meta": { + "requestGuid": "some-guid-string-here", + "timestamp": "2020-03-...", + "source": { + "desktopAgent": "agent-A", + "name": "...", + "appId": "...", + "version": "...", + // ... other metadata fields + }, + "requestPath": ["agent-A", "agent-C"] + } } ``` +When adding context listeners (either for User channels or specific App Channels) no messages need to be exchanged. Instead, upon receving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. + ## Intents ### findIntent ```typescript @@ -229,38 +234,58 @@ A findIntent call is made on agent-A. let appIntent = await fdc3.findIntent(); ``` -It sends an outward message to the other desktop agents (sent from A -> C): +Sends an outward message to the desktop agent(s) acting as server(s). + ```JSON +// agent-A -> agent-C { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "timestamp": "2020-03-...", "type": "findIntent", - "body": { - "intent": "StartChat", - "context": {/*contxtObj*/} + "payload": { + "intent": "StartChat", + "context": {/*contxtObj*/} + }, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + } } } ``` -which is repeated from C -> B as: +The server (agent-C) fills in the `source.desktopAgent` field and forwards the request to the other desktop agents. + ```JSON +// agent-C -> agent-B { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "timestamp": 2020-03-..., "type": "findIntent", - "body": { + "payload": { "intent": "StartChat", "context": {/*contxtObj*/}, }, - "sourceAgent": "agent-A" + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "desktoAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + }, + "requestPath": ["agent-A", "agent-C"] + } } ``` - -Note that the `sourceAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. +Note that the `source.desktopAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. #### Response format -Normal response from:agent A, where the request was raised (a websocket client) +Normal response from agent-A, where the request was raised (a websocket client) ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -283,14 +308,13 @@ Desktop agent B (a websocket client) woud produce response: } ``` -which is sent back over the bridge by Agent B -> C as a response to the request message as: +which is sent back over the bridge as a response to the request message as: + ```JSON +// agent-B -> agent-C { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "b4cf1b91-0b64-45b6-9f55-65503d507024", - "timestamp": 2020-03-..., - "type": "findIntent", - "body": { + "type": "findIntentResponse", + "payload": { "intent": "StartChat", "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -302,31 +326,59 @@ which is sent back over the bridge by Agent B -> C as a response to the request ] } }, - "targetAgent": "agent-A" + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "requestAgentBGuid", + "timestamp": "2020-03-...", + "destination": { + "desktopAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + }, + "responsePath": [ + ["agent-B", "agent-c"] + ] + } } ``` Which gets repeated by the websocket server (agent-C) in augmented form as: + ```JSON { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "b4cf1b91-0b64-45b6-9f55-65503d507024", - "timestamp": 2020-03-..., - "type": "findIntent", - "body": { + "type": "findIntentResponse", + "payload": { "intent": "StartChat", "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "Skype", "agent": "agent-B"}, - { "name": "Symphony", "agent": "agent-B" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "agent": "agent-B" }, - { "name": "Slack", "agent": "agent-B" } + { "name": "Skype", "desktopAgent": "agent-B"}, + { "name": "Symphony", "desktopAgent": "agent-B" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, + { "name": "Slack", "desktopAgent": "agent-B" } ] } }, - "targetAgent": "agent-A", - "sourceAgent": "agent-B" + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "requestAgentB_Guid", + "timestamp": "2020-03-...", + "destination": { + "desktopAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + }, + "source": { + "desktoAgent": "agent-B", + }, + "responsePath": [ + ["agent-B", "agent-c"] + ] + } } ``` @@ -343,34 +395,49 @@ Desktop agent C (the websocket server) also sends its own response: which it encodes as a message: ```JSON { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "988a49c8-49c2-4fb4-aad4-be39d1471834", - "timestamp": 2020-03-..., - "type": "findIntent", - "body": { + "type": "findIntentResponse", + "payload": { "intent": "StartChat", "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "WebIce", "agent": "agent-C"} + { "name": "WebIce", "desktopAgent": "agent-C"} ] } }, - "targetAgent": "agent-A", - "sourceAgent": "agent-C" + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "requestAgentC_Guid", + "timestamp": "2020-03-...", + "destination": { + "desktopAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + }, + "source": { + "desktoAgent": "agent-C", + }, + "responsePath": [ + ["agent-c"] + ] + } } ``` -Then on agent-A the originating app finally gets back the following response from the FDC3 desktop "agent-C": +Then on agent-A the originating app finally gets back the following response from agent-C: + ```JSON +// agent-C -> agent-A { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ { "name": "myChat" }, // local to this agent - { "name": "Skype", "agent": "agent-B" }, //agent-B responses - { "name": "Symphony", "agent": "agent-B" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "agent": "agent-B" }, - { "name": "Slack", "agent": "agent-B" }, - { "name": "WebIce", "agent": "agent-C"} //agent C response + { "name": "Skype", "desktopAgent": "agent-B" }, //agent-B responses + { "name": "Symphony", "desktopAgent": "agent-B" }, + { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, + { "name": "Slack", "desktopAgent": "agent-B" }, + { "name": "WebIce", "desktopAgent": "agent-C"} //agent C response ] } ``` @@ -380,282 +447,603 @@ Then on agent-A the originating app finally gets back the following response fro ```typescript raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; ``` -Note as IntentResolutions can now return a promise of result data there are multiple response formats required +For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead to collect options to display in a local resolver UI, allowing for a targetted intent to be raised afterwards. See details below. #### Request format -A raiseIntent call is made on agent-A. +A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. ```typescript -raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; +raiseIntent(intent: string, context: Context): Promise; ``` -__WIP__ +agent-A sends an outward `findIntent` message to the desktop agent(s) acting as server(s): -Just brain dumping stuff at the moment... - -``` -DA-A DA-B DA-C -| ---- raiseIntent ----> | | -| |---- raiseIntent --->| -| |<--- intentResolution ----| -|<-- resolutionOptions -----| | -|--- raiseIntent(... app)-->| | -| |------ raiseIntent ------->| -| |<--- intentResolution ----| -|<--- intentResolution -----| | +```JSON +// agent-A -> agent-C +{ + "type": "findIntent", + "payload": { + "intent": "StartChat", + "context": {/*contxtObj*/} + }, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "someOtherApp", //should this be the desktop agent or the app? + "appId": "...", + "version": "...", + // ... other metadata fields + } + } +} ``` ---- -**Assumptions & Questions** -- No client - client communication - All request go to server first which are then routed to the correct DA. -- The intentResolution result should be as is? The outcome of the raiseIntent should be the same as current? - -**NOTE:** - (TP 14/03/2022) - The raiseIntent response should not really change (?) - Meaning, you raise and intent and the result of that would either be an intentResolution, that might need to contain information about which DA the intent was resolved, OR an error if the intent fails to resolve. - -The raiseIntent request however, will need to be able to specify a target which must (should) include an DA as well. -Maybe we can strongly advise that `raiseIntent` should be preceeded by `findIntents`. +This will trigger the same flow as `findIntent`. Upon receiveing a `findIntentResponse`, the resolver is shown. -No target specified -1. DA-A (client) sends `raiseIntent` request without target to DA-C (server) -2. DA-C MUST fire a `findIntent` to DA-A, DA-B and DA-C - * `findIntent` response only has one possible resolution - * No resolver UI is shown and a `findIntent` response is sent to DA-A which sends a `raiseIntent` with target. This should happen silently/transparently? - * `findIntent` response has multiple resolution possibilities - * Resolver UI is shown on DA-A which upon the user selecting the app will send a `raiseIntent` with target - * `findIntent` returns error because there is no possible resolution +User selects an option which will trigger a `raiseIntent` call with a `app:TargetApp` argument. +--- -Target specified - -1. DA-A (client) sends `raiseIntent` request with target (DA-B) to DA-C (server) -2. DA-C populates the sourceAgent in the raiseIntent request and forward the request to DA-B directly -3. DA-B sends `intentResolution` response to DA-C that fills in the targetAgent field -4. DA-C sends the augmented `intentResolution` response to DA-A +A `raiseIntent` call is made on agent-A which targets an `AChatApp` in agent-B. ---- +```typescript +raiseIntent(intent: string, context: Context, app: TargetApp): Promise; +``` -It sends an outward message to the other desktop agents (sent from A -> C): ```JSON -// no target -{ - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", - "timestamp": "2020-03-...", - "type": "raiseIntent", - "body": { - "intent": "ViewChart", - "context": {/*contxtObj*/} - } -} -// with AppMetadata +// agent-A -> agent-C { - "requestGuid": "c916ee2e-feb7-437e-9ab3-be52ac46a6bc", - "timestamp": "2020-03-...", - "type": "raiseIntent", - "body": { - "intent": "ViewChart", - "context": {/*contxtObj*/}, - "app": { - "name": "AChartApp", - "agent": "agent-c" - } - } -} -// with AppName only -{ - "requestGuid": "2adbd9e5-5a22-4869-9a8c-95ca7ec8b6ae", - "timestamp": "2020-03-...", - "type": "raiseIntent", - "body": { - "intent": "ViewChart", - "context": {/*contxtObj*/}, - "app": "AChartApp" - } + "type": "raiseIntent", + "payload": { + "intent": "StartChat", + "context": {/*contxtObj*/}, + "app": { + "name": "AChatApp", + "desktopAgent": "agent-B" + } + }, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "someOtherApp", + "appId": "...", + "version": "...", + // ... other metadata fields + }, + "destination": { // duplicates the app argument so that the message is routed like any other + "app": { + "name": "AChatApp", + "desktopAgent": "agent-B" + } + } + } } ``` -Which is repeated from C -> B as: +The agent-C (server) fills in the `source.desktopAgent` field and forwards the request to the target desktop agent. ```JSON +// agent-C -> agent-B { - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", - "timestamp": 2020-03-..., "type": "raiseIntent", - "body": { - "intent": "ViewChart", + "payload": { + "intent": "StartChat", "context": {/*contxtObj*/}, }, - "sourceAgent": "agent-A" + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "someOtherApp", + "appId": "...", + "version": "...", + "desktopAgent": "agent-A" // <---- filled by server (C) + // ... other metadata fields + }, + "destination": { + "app": { + "name": "AChatApp", + "desktopAgent": "agent-B" + } + }, + "requestPath": ["agent-A", "agent-C"] + } } ``` -Note that the sourceAgent field has been populated with the id of the agent that raised the requests, enabling the routing of responses. + #### Response format -Normal response from:agent A, where the request was raised (a websocket client) +Normal response from agent-B (to-C), where the request was targeted to by agent-A. It sends this `intentResolution` as soon as it delivers the `raiseIntent` to the target app. + ```JSON +// agent-B -> agent-C { - "intent": "ViewChart", - "source": { - "name": "myChartA", + "type": "intentResolution", + "payload": { + "intent": "StartChat", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields + }, + "version": "...", }, - "resolution": { - // with a context - "context": {/*contxtObj*/} + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "responseGuid", + "timestamp": "2020-03-...", + "error?:": "ResolveError Enum", + "source": { //Note this was the destination of the raised intent + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields + }, + "destination": { + "app": { //note this was the source of the raised intent + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } + } } } ``` -or + +agent-C (server) will fill in the `source.DesktopAgent` and relay the message on to agent-A. + ```JSON +// agent-C -> agent-A { - "intent": "ViewChart", - "source": { - "name": "myChartA", + "type": "intentResolution", + "payload": { + "intent": "StartChat", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields + }, + "version": "...", }, - "resolution": { - // with a channel - "id": "channel 1", - "type": "system" + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "intentResolutionResponseGuid", + "timestamp": "2020-03-...", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } + }, + "responsePath": [ + ["agent-B", "agent-c"] + ] } } ``` -Desktop agent B (a websocket client) woud produce response: +When `AChatApp` produces a response, or the intent handler finishes running, it should send a further `intentResult` message to send that response onto the intent raiser (or throw an error if one occurred) + ```JSON +// agent-B -> agent-C -> agent-A { - "intent": "ViewChart", - "source": { - "name": "myChartB", + "type": "intentResult", + "payload?:": { + "channel": { + "id": "channel 1", + "type": "system" + }, + "context": {/*contextObj*/} // in alternative to channel }, - "resolution": { - "context": {/*contxtObj*/} + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "intentResultResponseGuid", + "timestamp": "2020-03-...", + "error?:": "ResultError Enum", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } + }, + "responsePath": [ + ["agent-B", "agent-c"] + ] } } ``` -which is sent back over the bridge by Agent B -> C as a response to the request message as: +If intent result is private channel: + ```JSON +// agent-B -> agent-C -> agent-A { - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", - "responseGuid": "b8a86c63-049e-469d-bc76-bd1d10b17344", - "timestamp": 2020-03-..., - "type": "raiseIntent", - "body": { - "intentResolution": { - "intent": "ViewChart", - "source": { - "name": "myChartB", - }, - "resolution": { - "context": {/*contxtObj*/} - } - } + "type": "intentResult", + "payload?:": { + "channel": { + "id": "channel a", + "type": "private" + }, + "context": {/*contextObj*/} // in alternative to channel }, - "targetAgent": "agent-A" + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "intentResultResponseGuid", + "timestamp": "2020-03-...", + "error?:": "ResultError Enum", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" // filled by server + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + } + }, + "responsePath": [ + ["agent-B", "agent-c"] + ] + } } ``` +--- +`onSubscribe` to the private channel sent to server: -Which gets repeated by the websocket server (agent-C) in augmented form as: ```JSON +// agent-A -> agent-C { - "requestGuid": "62fe69bd-3f40-45a4-86fc-0150dbade8ab", - "responseGuid": "b8a86c63-049e-469d-bc76-bd1d10b17344", - "timestamp": 2020-03-..., - "type": "raiseIntent", - "body": { - "intentResolution": { - "intent": "ViewChart", - "source": { - "name": "myChartB", - }, - "resolution": { - "context": {/*contxtObj*/} // or channel + "type": "privateChannelSubscribe", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields } } - }, - "targetAgent": "agent-A", - "sourceAgent": "agent-B" + } } ``` -Desktop agent C (the websocket server) also sends its own response: +Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) + ```JSON +// agent-A -> agent-C { - "intent": "ViewChart", - "source": { - "name": "myChartC", - }, - "resolution": { - "context": {/*contxtObj*/} + "type": "privateChannelSubscribe", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + }, + "requestPath": ["agent-A", "agent-C"] } } ``` +--- +`onUnsubscribe` to the private channel sent to server -which it encodes as a message: ```JSON +// agent-A -> agent-C { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", - "responseGuid": "1fe1a74f-2943-4ca4-b90a-afc317fd149c", - "timestamp": 2020-03-..., - "type": "raiseIntent", - "body": { - "intentResolution": { - "intent": "ViewChart", - "source": { - "name": "myChartC", - }, - "resolution": { - "context": {/*contxtObj*/} // or channel - } - } - }, - "targetAgent": "agent-A", - "sourceAgent": "agent-C" + "type": "privateChannelUnsubscribe", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } } ``` -Then agent C (ie. the server) should augment the responses so that a unequivocal resolution can be achieved (imagine 2 DA have app with the same name that are able to resolve the same intent) by agent-a. +Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// intent resolution response??? +// agent-C -> agent-B { - "intent": "ViewChart", - "sources": [ - "agent-A": { // agent-A intentResolution - "source": { - "name": "myChartA", - }, - }, - "agent-B": { // agent-B intentResolution - "source": { - "name": "myChartB", - }, - }, - "agent-c": { // agent-C intentResolution - "source": { - "name": "myChartC", - }, + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "privateChannelUnsubscribe", + "payload": {}, + "meta": { + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A", + // ... other metadata fields }, - ], + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + }, + "requestPath": ["agent-A", "agent-C"] + } } +``` +--- +`onDisconnect` to the private channel sent to server -// another option +```JSON +// agent-A -> agent-C { - "intent": "ViewChart", - "sources": [ - "source": { // agent-A intent resolver UI response - "name": "myChartA", - "agent": "agent-A" + "type": "privateChannelDisconnect", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + // ... other metadata fields }, - "source": { // agent-B intent resolver UI response - "name": "myChartB", - "agent": "agent-B" - }, - "source": { // agent-C intent resolver UI response - "name": "myChartC", - "agent": "agent-C" + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + +Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) + +```JSON +// agent-C -> agent-B +{ + "type": "privateChannelDisconnect", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "name": "AChatApp", + "appId": "", + "version": "", + "desktopAgent": "agent-A" + // ... other metadata fields }, - ], + "destination": { // duplicates the app argument + "app": { + "name": "someOtherApp", + "appId": "", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields + } + }, + "requestPath": ["agent-A", "agent-C"] + } +} +``` +--- + +### `fdc3.open` +```typescript + open(app: TargetApp, context?: Context): Promise; +``` +#### Request format: +A `fdc3.open`` call is made on agent-A. + +```javascript +// Open an app without context, using the app name +let instanceMetadata = await fdc3.open('myApp'); + +// Open an app without context, using an AppMetadata object to specify the target +let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; +let instanceMetadata = await fdc3.open(appMetadata); + +// Open an app without context, using an AppMetadata object to specify the target and Desktop Agent +let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgent:"DesktopAgentB"}; +let instanceMetadata = await fdc3.open(appMetadata); +``` + + +The `fdc3.open` command should result in a single copy of the specified app being opened and its instance data returned, or an error if it could not be opened. There are two possible scenarios: + +1) The Desktop Agent that the app should open on is specified +2) The Desktop Agent that the app should open on is NOT specified app + +The first case (target Desktop Agent is specified) is simple: +- If the local Desktop Agent is the target, handle the call normally +- If you are a server + - check if any of your clients is the target and transmit the call to them and await a response + - If you are also a client of another server follow the client steps + - otherwise return `OpenError.AppNotFound` +- If you are a client + - transmit the call to the server and await a response + +The second case is a little trickier as we don't know which agent may have the app available: +- If the local Desktop Agent has the app, open it and exit. +- If you are a server + - call each client one at a time and await a response + - If the response is `OpenError.AppNotFound` move to the next client + - If the response is AppMetadata then return it and exit + - If you are also a client of another server follow the client steps + - otherwise return `OpenError.AppNotFound` +- If you are a client + - transmit the call to the server and await a response + + + +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Open Chart + DB ->>+ DC: Open Chart + DB ->> DB: App Found + DB ->> DB: Open App + DB -->>- DA: Return App Data + DC ->> DC: App Found + DC ->> DC: Open App + DC -->>- DB: Return App Data +``` + +**When the target Desktop Agent is set** +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Open App + DB --x DB: Is not in the desktopAgents list + DB ->>+ DC: Open App + DC ->> DC: Desktop agent in list and App Found + DC ->> DC: Open App + DC -->>- DB: Return App Data + DB -->>- DA:Return App Data +``` + +It sends an outward message to the other desktop agents (sent from A -> C): +```JSON +{ + "type": "open", + "payload": { + "appMetaData": { + "name": "myApp", + "appId": "myApp-v1.0.1", + "version": "1.0.1", + "desktopAgent":"agent-B" + }, + "context": {/*contxtObj*/} + }, + "meta": { + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": "2020-03-...", + } +} +``` + +which is repeated from C -> B as: +```JSON +{ + "type": "open", + "payload": { + "appMetaData": { + "name": "myApp", + "appId": "myApp-v1.0.1", + "version": "1.0.1", + "desktopAgent":"DesktopAgentB" + }, + "context": {/*contxtObj*/} + }, + "meta": { + "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "timestamp": 2020-03-..., + "source": { + "desktoAgent": "agent-A", + // ... other metadata fields + } + }, + "requestPath": ["agent-A", "agent-C"] } ``` +### `fdc3.findInstances` +```typescript + findInstances(app: TargetApp): Promise>; +``` + + +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DB: Find Instances of App + DB ->>+ DC: Find Instances of App + DC -->>- DB: Return App Data + DB -->>- DA: Return App Data +``` + ## Channels +App Channels don't need specific messages sending for `fdc3.getOrCreateChannel` as other agents will be come aware of it when messages are broadcast. + +However, `PrivateChannel` instances do require additional handling due to the listeners for subscription and disconnect. Please see the raiseIntent section for the mesages sent in support of this functionality. From 1514499b4de5c789c0b264f2120bee14f7bd0340 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Fri, 1 Apr 2022 13:25:40 +0100 Subject: [PATCH 17/61] spec update --- docs/api-bridging/spec.md | 192 ++++++++++++++++++++++++++------------ 1 file changed, 130 insertions(+), 62 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 8a031d17c..936117737 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,49 +1,80 @@ # Desktop Agent Bridging -In order to implement Desktop Agent Bridging some means for Desktop Agents to communicate with each other is needed. One obvious solution is to support the use a websocket to communicate. However, to do so some idea of the protocol for that communication is needed. +In order to implement Desktop Agent Bridging some means for Desktop Agents to communicate with each other is needed. One obvious solution is to support the use of websockets to communicate. However, to do so, some idea of the protocol for that communication is needed. + +## Websocket protocol proposal -## Websocket protocol proposal In a typical bridging scenario, one of more Desktop Agents will need to provide a websocket server for other agents to connect to - in the rest of this proposal the agent providing the websocket server will be referred to as 'the server' or 'a server', while Desktop Agents connecting to it are referred to as 'the client' or 'a client'. If a Desktop Agent acts as a server, this should not preclude it also being a client of another server (allowing for a variety of bridging topologies). However, there should exist only one connection between any two desktop agents. +__Note:__ A Desktop Agent acting as a client can only be connected to one server at a given time. + ### Identifying Desktop Agents and Message Sources + In order to target intents and perform other actions that require specific routing between Desktop Agents, Desktop Agents need to have an identity. Identities should be assigned to clients when they connect to a server, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. -To prevent spoofing and to simplify the implementation of clients, sender identities for birdging messages should be added, by the server to top level messages AND to AppMetadata objects embedded in them. +To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the server to top level messages AND to `AppMetadata` objects embedded in them. -* Sender details to be added by websocket server to top level messages AND any embedded AppMetadata objects. - * AppMetadata needs a new `desktopAgent `field - * When a client connects to a server it should be assigned an identity of some sort, which can be used to augment messages with details of the agent - * The server should do the assignments and could generate ids or accept them via config. - * Clients don't need to know their own ids or even the ids of others, they just need to be able to pass around AppMetadata objects that contain them. +* Sender details to be added by websocket server to top level messages AND any embedded `AppMetadata` objects. + * `AppMetadata` needs a new `desktopAgent` field + * When a client connects to a server it should be assigned an identity of some sort, which can be used to augment messages with details of the agent + * The server should do the assignments and could generate ids or accept them via config. + * Clients don't need to know their own ids or even the ids of others, they just need to be able to pass around `AppMetadata` objects that contain them. ### Identifying Individual Messages + There are a variety of message types we'll need to send between bridged Desktop Agents, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. * GUIDs required to uniquely identify messages - * To be referenced in replies + * To be referenced in replies + +This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response the does not reference a request GUID should be considered invalid. ### Handling FDC3 calls When Bridged + * Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls. - * for resilience, this may mean defining timeouts... + * for resilience, this may mean defining timeouts... ### Forwarding of Messages from Other Agents -To enable support for a vairety of topologies, it is necessary for a Desktop Agent to be able to forward messages received from one Desktop Agent on to others. There are a few simple rules which determine whether a message needs to be forwarded: -- the message does not have a target Desktop Agent (e.g. findIntent) - - If you are a client of a server, send it on to the server - - If you are a server, send it on to your clients (except the source of the message) - - If you are both do both -- If the message has a target Destkop Agent (e.g. response to findIntent) - - If you are a server and the target is one of your clients forward the message to it. - - If you are a client (and teh target isn't you) forward the message to your server. +To enable support for a vairety of topologies, it is necessary for a Desktop Agent to be able to forward messages received from one Desktop Agent on to others, excluding the Desktop Agent where the request was originated. There are a few simple rules which determine whether a message needs to be forwarded: + +* the message does not have a target Desktop Agent (e.g. findIntent) + * If you are a client of a server, send it on to the server + * If you are a server, send it on to your clients (except the source of the message) + * If you are both do both + +* If the message has a target Destkop Agent (e.g. response to findIntent) + * If you are a server and the target is one of your clients forward the message to it. + * If you are a client (and teh target isn't you) forward the message to your server. + +### Message Paths + +This spec also introduces the concept of `requestPath` and `responsePath`. These paths will hold the Desktop Agents a given request/response has been through. It is a server responsbility to add this information. + +The use-cases for both the `requestPath` and the `responsePath` are, but not limited to, auditing, logging, debugging and message loop detection. + +__Note:__ In the case of message loop detection, this is driven by the topolgy of the Desktop Agents, which is still an open matter. ### Open Questions + * Is it necessary to preserve message path as it passes through different servers? +* Do we need to make mandatory that a `fdc3.joinChannel` was invoked before you can return the current state of the channel? +* Bridging startup - Consider an app that joins a channel whose last context was sent before the bridge was created, then DA couldn’t send the correct initial context when the channel joined + * Current context is ignored + * Add an init flow where DAs share info about channels they have with context on them already - (this type of discovery of channels does not happen in FDC3 at the moment, therefore it would add significant complexity to the spec). +* How to handle slow responding DAs? +* Having a decicated server/repeater vs. DA acting as server/repeater +| | Pros | Cons | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Dedicated server/repeater | • Easier discovery - port standardization (allow for config override)
• Simpler auth - each DA can be configured with an access key needed to connect to the bridge
• DAs only need to be tested against a single server implementation
• Conformance testing will be simpler
• Competing product won't have to interact directly
• Behavioural contracts will be easier to enforce through a single 'server' implementation
• DAs only need to implement the client portion of the proposal
• Message routing becomes trivial
• A single implementation can be contributed to FINOS and maintained by all
• The repeater could maintain channel state to provide to newly connected agents
• Collating responses and handling timeouts can happen in one place | • Single point of failure (there are some workarounds to this such as replica sets)
• Harder to customize to fit specific use-case scenarios
• Features and capabilities need to be agreed with multiple vendors | +| DA as server/repeater | • More control of server implementation | • Topology management
• Start up orchestration required | + + +### AppMetadata -### AppMetadata `AppMetadata` needs to be expanded to contain a `desktopAgent` field. ```typescript @@ -92,7 +123,10 @@ interface AppMetadata { ### Generic request and response formats -#### Request: +From simpliciy, in this document the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquelly identify resources should be used. + +#### Request + ```typescript { /** FDC3 function name message relates to, e.g. "findIntent" */ @@ -118,8 +152,10 @@ interface AppMetadata { } ``` -#### Response: -Responses will be differentiated by the presence of a `responseGuid` field. +#### Response + +Responses will be differentiated by the presence of a `responseGuid` field and MUST reference the requestGuid that they are responding to. + ```typescript { /** FDC3 function name the original request related to, e.g. "findIntent" */ @@ -147,24 +183,34 @@ Responses will be differentiated by the presence of a `responseGuid` field. } } ``` -Clients should send these messages on to the 'server', which will add the `source.desktopAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. +Clients should send these messages on to the 'server', which will add the `source.desktopAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. ### Individual message exchanges + The sections below cover most scenerios for each of the Desktop Agent methods in order to explore how this protocol might work. -Each section assumes that we have 3 agents connected by bridge: agent-A, agent-B and agent-C. Agent-C provides a websocket server that agent-A and agent-B have connected to. +Each section assumes that we have 3 agents connected by bridge: +* agent-A +* agent-B +* agent-C +agent-C provides a websocket server that agent-A and agent-B have connected to. ## Context + ### For broadcasts on channels + Only needs a single message (no response) An app on agent-A does: + ```javascript fdc3.broadcast(contextObj); ``` + or + ```javascript (await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` @@ -181,7 +227,7 @@ Message flow: agent-A -> agent-C "context": { /*contxtObj*/ } }, "meta": { - "requestGuid": "some-guid-string-here", + "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { "name": "...", @@ -193,11 +239,10 @@ Message flow: agent-A -> agent-C } ``` -which it repeats on to Agent-B with the `source.desktopAgent` and `requestPath` array metadata added. - -Message flow: agent-C -> agent-B +which it repeats on to agent-B with the `source.desktopAgent` and `requestPath` array metadata added. ```JSON +// agent-C -> agent-B { "type": "broadcast", "payload": { @@ -205,7 +250,7 @@ Message flow: agent-C -> agent-B "context": { /*contxtObj*/} }, "meta": { - "requestGuid": "some-guid-string-here", + "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { "desktopAgent": "agent-A", @@ -219,22 +264,25 @@ Message flow: agent-C -> agent-B } ``` -When adding context listeners (either for User channels or specific App Channels) no messages need to be exchanged. Instead, upon receving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. +When adding context listeners (either for User Channels or specific App Channels) no messages need to be exchanged. Instead, upon receving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. ## Intents + ### findIntent + ```typescript findIntent(intent: string, context?: Context): Promise; ``` -#### Request format: +#### Request format A findIntent call is made on agent-A. + ```javascript let appIntent = await fdc3.findIntent(); ``` -Sends an outward message to the desktop agent(s) acting as server(s). +Sends an outward message to the desktop agent acting as server. ```JSON // agent-A -> agent-C @@ -281,11 +329,13 @@ The server (agent-C) fills in the `source.desktopAgent` field and forwards the r } } ``` -Note that the `source.desktopAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. + +Note that the `source.desktopAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. Additionally a `requestPath` was added that will contain the source/originator of the request and the Desktop Agents it has "visited". #### Response format Normal response from agent-A, where the request was raised (a websocket client) + ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -296,6 +346,7 @@ Normal response from agent-A, where the request was raised (a websocket client) ``` Desktop agent B (a websocket client) woud produce response: + ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -336,10 +387,7 @@ which is sent back over the bridge as a response to the request message as: "appId": "", "version": "", // ... other metadata fields - }, - "responsePath": [ - ["agent-B", "agent-c"] - ] + } } } ``` @@ -383,6 +431,7 @@ Which gets repeated by the websocket server (agent-C) in augmented form as: ``` Desktop agent C (the websocket server) also sends its own response: + ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, @@ -393,6 +442,7 @@ Desktop agent C (the websocket server) also sends its own response: ``` which it encodes as a message: + ```JSON { "type": "findIntentResponse", @@ -425,6 +475,7 @@ which it encodes as a message: } } ``` + Then on agent-A the originating app finally gets back the following response from agent-C: ```JSON @@ -447,9 +498,13 @@ Then on agent-A the originating app finally gets back the following response fro ```typescript raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; ``` + For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead to collect options to display in a local resolver UI, allowing for a targetted intent to be raised afterwards. See details below. +When receiving a response from invoking `raiseIntent` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppMetadata` with an `instanceId`. + #### Request format + A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. ```typescript @@ -553,8 +608,8 @@ The agent-C (server) fills in the `source.desktopAgent` field and forwards the r } ``` - #### Response format + Normal response from agent-B (to-C), where the request was targeted to by agent-A. It sends this `intentResolution` as soon as it delivers the `raiseIntent` to the target app. ```JSON @@ -720,6 +775,7 @@ If intent result is private channel: } } ``` + --- `onSubscribe` to the private channel sent to server: @@ -780,6 +836,7 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message } } ``` + --- `onUnsubscribe` to the private channel sent to server @@ -840,6 +897,7 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message } } ``` + --- `onDisconnect` to the private channel sent to server @@ -900,14 +958,20 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message } } ``` + --- ### `fdc3.open` + ```typescript open(app: TargetApp, context?: Context): Promise; ``` -#### Request format: -A `fdc3.open`` call is made on agent-A. + +When receiving a response from invoking `fdc3.open` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppMetadata` with an `instanceId`. + +#### Request format + +A `fdc3.open` call is made on agent-A. ```javascript // Open an app without context, using the app name @@ -922,32 +986,32 @@ let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', deskt let instanceMetadata = await fdc3.open(appMetadata); ``` - The `fdc3.open` command should result in a single copy of the specified app being opened and its instance data returned, or an error if it could not be opened. There are two possible scenarios: -1) The Desktop Agent that the app should open on is specified +1) The Desktop Agent that the app should open on is specified 2) The Desktop Agent that the app should open on is NOT specified app -The first case (target Desktop Agent is specified) is simple: -- If the local Desktop Agent is the target, handle the call normally -- If you are a server - - check if any of your clients is the target and transmit the call to them and await a response - - If you are also a client of another server follow the client steps - - otherwise return `OpenError.AppNotFound` -- If you are a client - - transmit the call to the server and await a response +The first case (target Desktop Agent is specified) is simple: + +* If the local Desktop Agent is the target, handle the call normally +* If you are a server + * check if any of your clients is the target and transmit the call to them and await a response + * If you are also a client of another server follow the client steps + * otherwise return `OpenError.AppNotFound` +* If you are a client + * transmit the call to the server and await a response The second case is a little trickier as we don't know which agent may have the app available: -- If the local Desktop Agent has the app, open it and exit. -- If you are a server - - call each client one at a time and await a response - - If the response is `OpenError.AppNotFound` move to the next client - - If the response is AppMetadata then return it and exit - - If you are also a client of another server follow the client steps - - otherwise return `OpenError.AppNotFound` -- If you are a client - - transmit the call to the server and await a response +* If the local Desktop Agent has the app, open it and exit. +* If you are a server + * call each client one at a time and await a response + * If the response is `OpenError.AppNotFound` move to the next client + * If the response is `AppMetadata` then return it and exit + * If you are also a client of another server follow the client steps + * otherwise return `OpenError.AppNotFound` +* If you are a client + * transmit the call to the server and await a response ```mermaid @@ -965,7 +1029,8 @@ sequenceDiagram DC -->>- DB: Return App Data ``` -**When the target Desktop Agent is set** +__When the target Desktop Agent is set__ + ```mermaid sequenceDiagram participant DA as Desktop Agent A @@ -981,6 +1046,7 @@ sequenceDiagram ``` It sends an outward message to the other desktop agents (sent from A -> C): + ```JSON { "type": "open", @@ -1001,6 +1067,7 @@ It sends an outward message to the other desktop agents (sent from A -> C): ``` which is repeated from C -> B as: + ```JSON { "type": "open", @@ -1026,6 +1093,7 @@ which is repeated from C -> B as: ``` ### `fdc3.findInstances` + ```typescript findInstances(app: TargetApp): Promise>; ``` @@ -1043,7 +1111,7 @@ sequenceDiagram ``` ## Channels -App Channels don't need specific messages sending for `fdc3.getOrCreateChannel` as other agents will be come aware of it when messages are broadcast. -However, `PrivateChannel` instances do require additional handling due to the listeners for subscription and disconnect. Please see the raiseIntent section for the mesages sent in support of this functionality. +App Channels don't need specific messages sending for `fdc3.getOrCreateChannel` as other agents will be come aware of it when messages are broadcast. +However, `PrivateChannel` instances do require additional handling due to the listeners for subscription and disconnect. Please see the raiseIntent section for the mesages sent in support of this functionality. From 2f5051b0d45426e7ecd8dcbbd8f30cb0708c9a81 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Mon, 25 Apr 2022 18:31:06 +0100 Subject: [PATCH 18/61] spec update - standalone bridge --- docs/api-bridging/spec.md | 371 ++++++++++++++++++++++---------------- 1 file changed, 217 insertions(+), 154 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 936117737..3ff2a8cef 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,77 +1,83 @@ # Desktop Agent Bridging -In order to implement Desktop Agent Bridging some means for Desktop Agents to communicate with each other is needed. One obvious solution is to support the use of websockets to communicate. However, to do so, some idea of the protocol for that communication is needed. +In order to implement Desktop Agent Bridging some means for Desktop Agents to communicate with each other is needed. This spec assumes the Desktop Agent Bridging is implemented via a standalone bridge (instead of peer-to-peer or client/server topologies). Another assumption of this spec is that the data traffic will be over websocket connection. -## Websocket protocol proposal +This topology will be similar to a star topology on a network in which the Desktop Agent Bridge (DAB or simply bridge) will be the central node acting as a router. -In a typical bridging scenario, one of more Desktop Agents will need to provide a websocket server for other agents to connect to - in the rest of this proposal the agent providing the websocket server will be referred to as 'the server' or 'a server', while Desktop Agents connecting to it are referred to as 'the client' or 'a client'. +The discovery, i.e. mechanism which allows us to discover which Desktop Agents (DA) are present in the "network" can be done via known port (TBD) or config. -If a Desktop Agent acts as a server, this should not preclude it also being a client of another server (allowing for a variety of bridging topologies). However, there should exist only one connection between any two desktop agents. +How the data will flow from Desktop Agent (DA) over a bridge to other DA will be outlined below. -__Note:__ A Desktop Agent acting as a client can only be connected to one server at a given time. +## Locating -### Identifying Desktop Agents and Message Sources +A DAB will implement a "server" behavior by: -In order to target intents and perform other actions that require specific routing between Desktop Agents, Desktop Agents need to have an identity. Identities should be assigned to clients when they connect to a server, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. +* receiving requests from clients +* route requests to clients +* route responses to clients -To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the server to top level messages AND to `AppMetadata` objects embedded in them. +A DA will implement a "client" behavior by: -* Sender details to be added by websocket server to top level messages AND any embedded `AppMetadata` objects. - * `AppMetadata` needs a new `desktopAgent` field - * When a client connects to a server it should be assigned an identity of some sort, which can be used to augment messages with details of the agent - * The server should do the assignments and could generate ids or accept them via config. - * Clients don't need to know their own ids or even the ids of others, they just need to be able to pass around `AppMetadata` objects that contain them. +* forwarding requests to the bridge +* await response(s) from the bridge +* receive requests from the bridge -### Identifying Individual Messages +## Connecting -There are a variety of message types we'll need to send between bridged Desktop Agents, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. +Desktop Agents should authenticate against the bridge, which needs to implement the authentication logic (TBD - access keys? JWT?). -* GUIDs required to uniquely identify messages - * To be referenced in replies +The DAB is also responsible for assigning each DA a name. a name can be requested by a DA. -This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response the does not reference a request GUID should be considered invalid. +Whilst the DAB represents a single point of failure in this bridging configuration, a critical failure should only mean that a DA will operate as if it was the only DA in a machine. + +## Interacting + +With a standalone DAB, the message paths and message propagation should become simple to implement since messages will only from between a source and a destination with a DAB in the middle. A standalone DAB will also simplify the implementation for supporting multi-machine and Access Control Lists. ### Handling FDC3 calls When Bridged -* Desktop agents that are bridged will need to wait for responses from other desktop agents before responding to API calls. +* DAs that are bridged will need to wait for responses from other DAs before responding to API calls. * for resilience, this may mean defining timeouts... -### Forwarding of Messages from Other Agents +### Identifying Desktop Agents Identity and Message Sources -To enable support for a vairety of topologies, it is necessary for a Desktop Agent to be able to forward messages received from one Desktop Agent on to others, excluding the Desktop Agent where the request was originated. There are a few simple rules which determine whether a message needs to be forwarded: +In order to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. -* the message does not have a target Desktop Agent (e.g. findIntent) - * If you are a client of a server, send it on to the server - * If you are a server, send it on to your clients (except the source of the message) - * If you are both do both +To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppMetadata` objects embedded in them. -* If the message has a target Destkop Agent (e.g. response to findIntent) - * If you are a server and the target is one of your clients forward the message to it. - * If you are a client (and teh target isn't you) forward the message to your server. +* Sender details to be added by the DAB to the embedded `AppMetadata` objects. + * `AppMetadata` needs a new `desktopAgent` field + * When a client connects to a DAB it should be assigned an identity of some sort, which can be used to augment messages with details of the agent + * The DAB should do the assignments and could generate ids or accept them via config. + * DAs don't need to know their own ids or even the ids of others, they just need to be able to pass around `AppMetadata` objects that contain them. -### Message Paths +### Identifying Individual Messages -This spec also introduces the concept of `requestPath` and `responsePath`. These paths will hold the Desktop Agents a given request/response has been through. It is a server responsbility to add this information. +There are a variety of message types we'll need to send between bridged DAs, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. -The use-cases for both the `requestPath` and the `responsePath` are, but not limited to, auditing, logging, debugging and message loop detection. +* GUIDs required to uniquely identify messages + * To be referenced in replies -__Note:__ In the case of message loop detection, this is driven by the topolgy of the Desktop Agents, which is still an open matter. +This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response the does not reference a request GUID should be considered invalid. + +### Forwarding of Messages from Other Agents + +The DAB MUST be able to forward messages received from one DA on to others (excluding obviously the Desktop Agent where the request was originated). There are a few simple rules which determine whether a message needs to be forwarded: + +* the message does not have a target Desktop Agent (e.g. findIntent) + * If you are a DA, send it on to the bridge + * The bridge will send it on to other known DAs (except the source of the message) + +* If the message has a target Destkop Agent (e.g. response to findIntent) + * The bridge will forward the message to it. ### Open Questions -* Is it necessary to preserve message path as it passes through different servers? * Do we need to make mandatory that a `fdc3.joinChannel` was invoked before you can return the current state of the channel? * Bridging startup - Consider an app that joins a channel whose last context was sent before the bridge was created, then DA couldn’t send the correct initial context when the channel joined * Current context is ignored * Add an init flow where DAs share info about channels they have with context on them already - (this type of discovery of channels does not happen in FDC3 at the moment, therefore it would add significant complexity to the spec). * How to handle slow responding DAs? -* Having a decicated server/repeater vs. DA acting as server/repeater - -| | Pros | Cons | -| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Dedicated server/repeater | • Easier discovery - port standardization (allow for config override)
• Simpler auth - each DA can be configured with an access key needed to connect to the bridge
• DAs only need to be tested against a single server implementation
• Conformance testing will be simpler
• Competing product won't have to interact directly
• Behavioural contracts will be easier to enforce through a single 'server' implementation
• DAs only need to implement the client portion of the proposal
• Message routing becomes trivial
• A single implementation can be contributed to FINOS and maintained by all
• The repeater could maintain channel state to provide to newly connected agents
• Collating responses and handling timeouts can happen in one place | • Single point of failure (there are some workarounds to this such as replica sets)
• Harder to customize to fit specific use-case scenarios
• Features and capabilities need to be agreed with multiple vendors | -| DA as server/repeater | • More control of server implementation | • Topology management
• Start up orchestration required | - ### AppMetadata @@ -123,7 +129,7 @@ interface AppMetadata { ### Generic request and response formats -From simpliciy, in this document the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquelly identify resources should be used. +From simpliciy, in this spec the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquelly identify resources should be used. #### Request @@ -145,9 +151,7 @@ From simpliciy, in this document the request and response GUID will be just `req /** Timestamp at which request was generated */ timestamp: date, /** AppMetadata source request received from */ - source?: AppMetadata, - /** The path (Desktop Agents) the request went through */ - requestPath?: string[] + source?: AppMetadata } } ``` @@ -177,14 +181,12 @@ Responses will be differentiated by the presence of a `responseGuid` field and M /** AppMetadata source request received from */ source?: AppMetadata, /** AppMetadata destination response sent from */ - destination?: AppMetadata, - /** The path (Desktop Agents) the responses came from */ - responsePaths?: string[][] + destination?: AppMetadata } } ``` -Clients should send these messages on to the 'server', which will add the `source.desktopAgent` metadata. Further, when processing responses, the agent acting as the 'server' should augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. +DAs should send these messages on to the bridge, which will add the `source.desktopAgent` metadata. Further, when processing responses, the bridge should also augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. ### Individual message exchanges @@ -195,14 +197,14 @@ Each section assumes that we have 3 agents connected by bridge: * agent-A * agent-B * agent-C - -agent-C provides a websocket server that agent-A and agent-B have connected to. +* dab ## Context ### For broadcasts on channels -Only needs a single message (no response) +Only needs a single message (no response). + An app on agent-A does: ```javascript @@ -215,11 +217,21 @@ or (await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` -It encodes this as a message which it sends to the websocket server (agent-C) +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DAB: Broadcast + DAB ->>+ DB: Broadcast + DAB ->>+ DC: Broadcast +``` -Message flow: agent-A -> agent-C +It encodes this as a message which it sends to the DAB ```JSON +// agent-a -> dab { "type": "broadcast", "payload": { @@ -239,10 +251,11 @@ Message flow: agent-A -> agent-C } ``` -which it repeats on to agent-B with the `source.desktopAgent` and `requestPath` array metadata added. +which it repeats on to agent-B AND agent-c with the `source.desktopAgent` metadata added. ```JSON -// agent-C -> agent-B +// dab -> agent-b +// dab -> agent-c { "type": "broadcast", "payload": { @@ -259,12 +272,11 @@ which it repeats on to agent-B with the `source.desktopAgent` and `requestPath` "version": "...", // ... other metadata fields }, - "requestPath": ["agent-A", "agent-C"] } } ``` -When adding context listeners (either for User Channels or specific App Channels) no messages need to be exchanged. Instead, upon receving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. +When adding context listeners (either for User Channels or specific App Channels) no messages need to be exchanged. Instead, upon receving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. ## Intents @@ -274,6 +286,20 @@ When adding context listeners (either for User Channels or specific App Channels findIntent(intent: string, context?: Context): Promise; ``` +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DAB: findIntent + DAB ->>+ DB: findIntent + DB -->>- DAB: findIntentResponse (B) + DAB ->>+ DC: findIntent + DC -->>- DAB: findIntentResponse (C) + DAB -->>- DA: findIntentResponse (B + C) +``` + #### Request format A findIntent call is made on agent-A. @@ -282,10 +308,10 @@ A findIntent call is made on agent-A. let appIntent = await fdc3.findIntent(); ``` -Sends an outward message to the desktop agent acting as server. +Sends an outward message to the DAB. ```JSON -// agent-A -> agent-C +// agent-a -> dab { "type": "findIntent", "payload": { @@ -305,10 +331,11 @@ Sends an outward message to the desktop agent acting as server. } ``` -The server (agent-C) fills in the `source.desktopAgent` field and forwards the request to the other desktop agents. +The DAB fills in the `source.desktopAgent` field and forwards the request to the other desktop agents (agent-b AND agent-c). ```JSON -// agent-C -> agent-B +// dab -> agent-b +// dab -> agent-c { "type": "findIntent", "payload": { @@ -319,22 +346,21 @@ The server (agent-C) fills in the `source.desktopAgent` field and forwards the r "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { - "desktoAgent": "agent-A", + "desktoAgent": "agent-A", // filled by dab "name": "", "appId": "", "version": "", // ... other metadata fields - }, - "requestPath": ["agent-A", "agent-C"] + } } } ``` -Note that the `source.desktopAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. Additionally a `requestPath` was added that will contain the source/originator of the request and the Desktop Agents it has "visited". +Note that the `source.desktopAgent` field has been populated with the id of the agent that raised the requests, enabling the routing of responses. #### Response format -Normal response from agent-A, where the request was raised (a websocket client) +Normal response from agent-A, where the request was raised. ```JSON { @@ -345,7 +371,7 @@ Normal response from agent-A, where the request was raised (a websocket client) } ``` -Desktop agent B (a websocket client) woud produce response: +DA agent-b woud produce response: ```JSON { @@ -362,7 +388,7 @@ Desktop agent B (a websocket client) woud produce response: which is sent back over the bridge as a response to the request message as: ```JSON -// agent-B -> agent-C +// agent-b -> dab { "type": "findIntentResponse", "payload": { @@ -379,7 +405,7 @@ which is sent back over the bridge as a response to the request message as: }, "meta": { "requestGuid": "requestGuid", - "responseGuid": "requestAgentBGuid", + "responseGuid": "responseGuidAgentB", "timestamp": "2020-03-...", "destination": { "desktopAgent": "agent-A", @@ -392,7 +418,9 @@ which is sent back over the bridge as a response to the request message as: } ``` -Which gets repeated by the websocket server (agent-C) in augmented form as: +Note the response guid generated by the agent-b and the reference to the request guid produced by agent-a where the request was originated. + +This response gets repeated by the bridge in augmented form as: ```JSON { @@ -411,7 +439,7 @@ Which gets repeated by the websocket server (agent-C) in augmented form as: }, "meta": { "requestGuid": "requestGuid", - "responseGuid": "requestAgentB_Guid", + "responseGuid": "responseGuidAgentB", "timestamp": "2020-03-...", "destination": { "desktopAgent": "agent-A", @@ -422,15 +450,12 @@ Which gets repeated by the websocket server (agent-C) in augmented form as: }, "source": { "desktoAgent": "agent-B", - }, - "responsePath": [ - ["agent-B", "agent-c"] - ] + } } } ``` -Desktop agent C (the websocket server) also sends its own response: +DA agent-c woud produce response: ```JSON { @@ -441,9 +466,10 @@ Desktop agent C (the websocket server) also sends its own response: } ``` -which it encodes as a message: +which is sent back over the bridge as a response to the request message as: ```JSON +// agent-c -> dab { "type": "findIntentResponse", "payload": { @@ -457,7 +483,36 @@ which it encodes as a message: }, "meta": { "requestGuid": "requestGuid", - "responseGuid": "requestAgentC_Guid", + "responseGuid": "responseGuidAgentC", + "timestamp": "2020-03-...", + "destination": { + "desktopAgent": "agent-A", + "name": "", + "appId": "", + "version": "", + // ... other metadata fields + } + } +} +``` + +This response gets repeated by the bridge in augmented form as: + +```JSON +{ + "type": "findIntentResponse", + "payload": { + "intent": "StartChat", + "appIntent": { + "intent": { "name": "StartChat", "displayName": "Chat" }, + "apps": [ + { "name": "WebIce", "desktopAgent": "agent-C"} + ] + } + }, + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "responseGuidAgentC", "timestamp": "2020-03-...", "destination": { "desktopAgent": "agent-A", @@ -467,19 +522,16 @@ which it encodes as a message: // ... other metadata fields }, "source": { - "desktoAgent": "agent-C", - }, - "responsePath": [ - ["agent-c"] - ] + "desktoAgent": "agent-C", + } } } ``` -Then on agent-A the originating app finally gets back the following response from agent-C: +Then on agent-A the originating app finally gets back the following response from the bridge: ```JSON -// agent-C -> agent-A +// dab -> agent-A { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ @@ -503,6 +555,24 @@ For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetA When receiving a response from invoking `raiseIntent` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppMetadata` with an `instanceId`. + +Note that the below diagram assumes a `raiseIntent` WITH a `app:TargetApp` was specified and therefore agent-C is not involved. + +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DAB: raiseIntent + DAB ->>+ DB: raiseIntent + DB -->>- DAB: intentResolution + DAB -->>- DA: intentResolution + DB ->>+ DAB: intentResult + DAB ->>+ DA: intentResult +``` + + #### Request format A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. @@ -511,10 +581,10 @@ A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. raiseIntent(intent: string, context: Context): Promise; ``` -agent-A sends an outward `findIntent` message to the desktop agent(s) acting as server(s): +agent-A sends an outward `findIntent` message to the DAB: ```JSON -// agent-A -> agent-C +// agent-a -> dab { "type": "findIntent", "payload": { @@ -547,7 +617,7 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise agent-C +// agent-a -> dab { "type": "raiseIntent", "payload": { @@ -577,10 +647,10 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise agent-B +// dab -> agent-B { "type": "raiseIntent", "payload": { @@ -594,7 +664,7 @@ The agent-C (server) fills in the `source.desktopAgent` field and forwards the r "name": "someOtherApp", "appId": "...", "version": "...", - "desktopAgent": "agent-A" // <---- filled by server (C) + "desktopAgent": "agent-A" // <---- filled by DAB // ... other metadata fields }, "destination": { @@ -603,17 +673,16 @@ The agent-C (server) fills in the `source.desktopAgent` field and forwards the r "desktopAgent": "agent-B" } }, - "requestPath": ["agent-A", "agent-C"] } } ``` #### Response format -Normal response from agent-B (to-C), where the request was targeted to by agent-A. It sends this `intentResolution` as soon as it delivers the `raiseIntent` to the target app. +Normal response from agent-B, where the request was targeted to by agent-A. It sends this `intentResolution` as soon as it delivers the `raiseIntent` to the target app ```JSON -// agent-B -> agent-C +// agent-B -> DAB { "type": "intentResolution", "payload": { @@ -628,7 +697,7 @@ Normal response from agent-B (to-C), where the request was targeted to by agent- }, "meta": { "requestGuid": "requestGuid", - "responseGuid": "responseGuid", + "responseGuid": "intentResolutionResponseGuid", "timestamp": "2020-03-...", "error?:": "ResolveError Enum", "source": { //Note this was the destination of the raised intent @@ -650,10 +719,10 @@ Normal response from agent-B (to-C), where the request was targeted to by agent- } ``` -agent-C (server) will fill in the `source.DesktopAgent` and relay the message on to agent-A. +The bridge will fill in the `source.DesktopAgent` and relay the message on to agent-A ```JSON -// agent-C -> agent-A +// dab -> agent-A { "type": "intentResolution", "payload": { @@ -662,7 +731,7 @@ agent-C (server) will fill in the `source.DesktopAgent` and relay the message on "name": "AChatApp", "appId": "", "version": "", - "desktopAgent": "agent-B" // filled by server + "desktopAgent": "agent-B" // filled by DAB // ... other metadata fields }, "version": "...", @@ -675,7 +744,7 @@ agent-C (server) will fill in the `source.DesktopAgent` and relay the message on "name": "AChatApp", "appId": "", "version": "", - "desktopAgent": "agent-B" // filled by server + "desktopAgent": "agent-B" // filled by DAB // ... other metadata fields }, "destination": { // duplicates the app argument @@ -686,10 +755,7 @@ agent-C (server) will fill in the `source.DesktopAgent` and relay the message on "desktopAgent": "agent-A" // ... other metadata fields } - }, - "responsePath": [ - ["agent-B", "agent-c"] - ] + } } } ``` @@ -697,7 +763,7 @@ agent-C (server) will fill in the `source.DesktopAgent` and relay the message on When `AChatApp` produces a response, or the intent handler finishes running, it should send a further `intentResult` message to send that response onto the intent raiser (or throw an error if one occurred) ```JSON -// agent-B -> agent-C -> agent-A +// agent-B -> DAB -> agent-A { "type": "intentResult", "payload?:": { @@ -716,7 +782,7 @@ When `AChatApp` produces a response, or the intent handler finishes running, it "name": "AChatApp", "appId": "", "version": "", - "desktopAgent": "agent-B" // filled by server + "desktopAgent": "agent-B" // filled by DAB // ... other metadata fields }, "destination": { // duplicates the app argument @@ -727,10 +793,7 @@ When `AChatApp` produces a response, or the intent handler finishes running, it "desktopAgent": "agent-A" // ... other metadata fields } - }, - "responsePath": [ - ["agent-B", "agent-c"] - ] + } } } ``` @@ -738,7 +801,7 @@ When `AChatApp` produces a response, or the intent handler finishes running, it If intent result is private channel: ```JSON -// agent-B -> agent-C -> agent-A +// agent-B -> DAB -> agent-A { "type": "intentResult", "payload?:": { @@ -757,7 +820,7 @@ If intent result is private channel: "name": "AChatApp", "appId": "", "version": "", - "desktopAgent": "agent-B" // filled by server + "desktopAgent": "agent-B" // filled by DAB // ... other metadata fields }, "destination": { // duplicates the app argument @@ -768,10 +831,7 @@ If intent result is private channel: "desktopAgent": "agent-A" // ... other metadata fields } - }, - "responsePath": [ - ["agent-B", "agent-c"] - ] + } } } ``` @@ -780,7 +840,7 @@ If intent result is private channel: `onSubscribe` to the private channel sent to server: ```JSON -// agent-A -> agent-C +// agent-A -> dab { "type": "privateChannelSubscribe", "payload": {}, @@ -806,10 +866,10 @@ If intent result is private channel: } ``` -Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) +The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// agent-A -> agent-C +// dab -> agent-b { "type": "privateChannelSubscribe", "payload": {}, @@ -831,17 +891,16 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message "desktopAgent": "agent-B" // ... other metadata fields } - }, - "requestPath": ["agent-A", "agent-C"] + } } } ``` --- -`onUnsubscribe` to the private channel sent to server +`onUnsubscribe` to the private channel sent to the bridge ```JSON -// agent-A -> agent-C +// agent-A -> dab { "type": "privateChannelUnsubscribe", "payload": {}, @@ -867,10 +926,10 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message } ``` -Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) +The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// agent-C -> agent-B +// dab -> agent-B { "requestGuid": "requestGuid", "timestamp": "2020-03-...", @@ -892,17 +951,16 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message "desktopAgent": "agent-B" // ... other metadata fields } - }, - "requestPath": ["agent-A", "agent-C"] + } } } ``` --- -`onDisconnect` to the private channel sent to server +`onDisconnect` to the private channel sent to the bridge ```JSON -// agent-A -> agent-C +// agent-A -> dab { "type": "privateChannelDisconnect", "payload": {}, @@ -928,10 +986,10 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message } ``` -Server (agent-C) will add in the source agent (agent-A) and forward the message to destination (agent-B) +The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// agent-C -> agent-B +// dab -> agent-B { "type": "privateChannelDisconnect", "payload": {}, @@ -953,8 +1011,7 @@ Server (agent-C) will add in the source agent (agent-A) and forward the message "desktopAgent": "agent-B" // ... other metadata fields } - }, - "requestPath": ["agent-A", "agent-C"] + } } } ``` @@ -1013,20 +1070,23 @@ The second case is a little trickier as we don't know which agent may have the a * If you are a client * transmit the call to the server and await a response - + ```mermaid sequenceDiagram participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C - DA ->>+ DB: Open Chart - DB ->>+ DC: Open Chart + DA ->>+ DAB: Open Chart + DAB ->>+ DB: Open Chart + DAB ->>+ DC: Open Chart DB ->> DB: App Found DB ->> DB: Open App - DB -->>- DA: Return App Data + DB -->>- DAB: Return App Data DC ->> DC: App Found DC ->> DC: Open App - DC -->>- DB: Return App Data + DC -->>- DAB: Return App Data + DAB -->>- DA: Augmented App Data ``` __When the target Desktop Agent is set__ @@ -1034,20 +1094,21 @@ __When the target Desktop Agent is set__ ```mermaid sequenceDiagram participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C - DA ->>+ DB: Open App - DB --x DB: Is not in the desktopAgents list - DB ->>+ DC: Open App + DA ->>+ DAB: Open App + DAB ->>+ DC: Open App DC ->> DC: Desktop agent in list and App Found - DC ->> DC: Open App - DC -->>- DB: Return App Data - DB -->>- DA:Return App Data + DC ->>x DC: Open App + DC -->>- DAB: Return App Data + DAB -->>- DA: Return App Data ``` -It sends an outward message to the other desktop agents (sent from A -> C): +It sends an outward message to the bridge: ```JSON +// agent-A -> DAB { "type": "open", "payload": { @@ -1060,15 +1121,16 @@ It sends an outward message to the other desktop agents (sent from A -> C): "context": {/*contxtObj*/} }, "meta": { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "requestGuid": "requestGuid", "timestamp": "2020-03-...", } } ``` -which is repeated from C -> B as: +which is repeated as: ```JSON +// DAB -> agent-B { "type": "open", "payload": { @@ -1081,14 +1143,13 @@ which is repeated from C -> B as: "context": {/*contxtObj*/} }, "meta": { - "requestGuid": "4dd60b3b-9835-4cab-870c-6b9b099ed7ae", + "requestGuid": "requestGuid", "timestamp": 2020-03-..., "source": { - "desktoAgent": "agent-A", + "desktoAgent": "agent-A", // filled by DAB // ... other metadata fields } - }, - "requestPath": ["agent-A", "agent-C"] + } } ``` @@ -1098,15 +1159,17 @@ which is repeated from C -> B as: findInstances(app: TargetApp): Promise>; ``` - ```mermaid sequenceDiagram participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C - DA ->>+ DB: Find Instances of App - DB ->>+ DC: Find Instances of App - DC -->>- DB: Return App Data + DA ->>+ DAB: Find Instances of App. + DAB ->>+ DB: Find Instances of App + DAB ->>+ DC: Find Instances of App + DC --x DC: No Instance found + DB ->> DB: App Instance found DB -->>- DA: Return App Data ``` From 09afeb07c9f5fa9012dd29e60835f15a2aed41b1 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 3 May 2022 11:35:54 +0100 Subject: [PATCH 19/61] Typos --- docs/api-bridging/spec.md | 45 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 3ff2a8cef..1c773f310 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -68,7 +68,7 @@ The DAB MUST be able to forward messages received from one DA on to others (excl * If you are a DA, send it on to the bridge * The bridge will send it on to other known DAs (except the source of the message) -* If the message has a target Destkop Agent (e.g. response to findIntent) +* If the message has a target Desktop Agent (e.g. response to findIntent) * The bridge will forward the message to it. ### Open Questions @@ -129,7 +129,7 @@ interface AppMetadata { ### Generic request and response formats -From simpliciy, in this spec the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquelly identify resources should be used. +For simplicity, in this spec the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquely identify resources should be used. #### Request @@ -190,9 +190,9 @@ DAs should send these messages on to the bridge, which will add the `source.desk ### Individual message exchanges -The sections below cover most scenerios for each of the Desktop Agent methods in order to explore how this protocol might work. +The sections below cover most scenarios for each of the Desktop Agent methods in order to explore how this protocol might work. -Each section assumes that we have 3 agents connected by bridge: +Each section assumes that we have 3 agents connected by a bridge: * agent-A * agent-B @@ -236,7 +236,7 @@ It encodes this as a message which it sends to the DAB "type": "broadcast", "payload": { "channel": "myChannel", - "context": { /*contxtObj*/ } + "context": { /*contextObj*/ } }, "meta": { "requestGuid": "requestGuid", @@ -260,7 +260,7 @@ which it repeats on to agent-B AND agent-c with the `source.desktopAgent` metada "type": "broadcast", "payload": { "channel": "myChannel", - "context": { /*contxtObj*/} + "context": { /*contextObj*/} }, "meta": { "requestGuid": "requestGuid", @@ -276,7 +276,7 @@ which it repeats on to agent-B AND agent-c with the `source.desktopAgent` metada } ``` -When adding context listeners (either for User Channels or specific App Channels) no messages need to be exchanged. Instead, upon receving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. +When adding context listeners (either for User Channels or specific App Channels) no messages need to be exchanged. Instead, upon receiving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. ## Intents @@ -316,7 +316,7 @@ Sends an outward message to the DAB. "type": "findIntent", "payload": { "intent": "StartChat", - "context": {/*contxtObj*/} + "context": {/*contextObj*/} }, "meta": { "requestGuid": "requestGuid", @@ -340,13 +340,13 @@ The DAB fills in the `source.desktopAgent` field and forwards the request to the "type": "findIntent", "payload": { "intent": "StartChat", - "context": {/*contxtObj*/}, + "context": {/*contextObj*/}, }, "meta": { "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { - "desktoAgent": "agent-A", // filled by dab + "desktopAgent": "agent-A", // filled by dab "name": "", "appId": "", "version": "", @@ -371,7 +371,7 @@ Normal response from agent-A, where the request was raised. } ``` -DA agent-b woud produce response: +DA agent-b would produce response: ```JSON { @@ -449,13 +449,13 @@ This response gets repeated by the bridge in augmented form as: // ... other metadata fields }, "source": { - "desktoAgent": "agent-B", + "desktopAgent": "agent-B", } } } ``` -DA agent-c woud produce response: +DA agent-c would produce response: ```JSON { @@ -522,7 +522,7 @@ This response gets repeated by the bridge in augmented form as: // ... other metadata fields }, "source": { - "desktoAgent": "agent-C", + "desktopAgent": "agent-C", } } } @@ -551,11 +551,10 @@ Then on agent-A the originating app finally gets back the following response fro raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; ``` -For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead to collect options to display in a local resolver UI, allowing for a targetted intent to be raised afterwards. See details below. +For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead to collect options to display in a local resolver UI, allowing for a targeted intent to be raised afterwards. See details below. When receiving a response from invoking `raiseIntent` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppMetadata` with an `instanceId`. - Note that the below diagram assumes a `raiseIntent` WITH a `app:TargetApp` was specified and therefore agent-C is not involved. ```mermaid @@ -589,7 +588,7 @@ agent-A sends an outward `findIntent` message to the DAB: "type": "findIntent", "payload": { "intent": "StartChat", - "context": {/*contxtObj*/} + "context": {/*contextObj*/} }, "meta": { "requestGuid": "requestGuid", @@ -604,7 +603,7 @@ agent-A sends an outward `findIntent` message to the DAB: } ``` -This will trigger the same flow as `findIntent`. Upon receiveing a `findIntentResponse`, the resolver is shown. +This will trigger the same flow as `findIntent`. Upon receiving a `findIntentResponse`, the resolver is shown. User selects an option which will trigger a `raiseIntent` call with a `app:TargetApp` argument. @@ -622,7 +621,7 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise Date: Tue, 3 May 2022 13:43:48 +0100 Subject: [PATCH 20/61] Headings and desktop agent name consistentcy --- docs/api-bridging/spec.md | 60 ++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 1c773f310..bf4e1b26c 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -71,7 +71,7 @@ The DAB MUST be able to forward messages received from one DA on to others (excl * If the message has a target Desktop Agent (e.g. response to findIntent) * The bridge will forward the message to it. -### Open Questions +## Open Questions * Do we need to make mandatory that a `fdc3.joinChannel` was invoked before you can return the current state of the channel? * Bridging startup - Consider an app that joins a channel whose last context was sent before the bridge was created, then DA couldn’t send the correct initial context when the channel joined @@ -79,7 +79,7 @@ The DAB MUST be able to forward messages received from one DA on to others (excl * Add an init flow where DAs share info about channels they have with context on them already - (this type of discovery of channels does not happen in FDC3 at the moment, therefore it would add significant complexity to the spec). * How to handle slow responding DAs? -### AppMetadata +## AppMetadata `AppMetadata` needs to be expanded to contain a `desktopAgent` field. @@ -127,11 +127,11 @@ interface AppMetadata { } ``` -### Generic request and response formats +## Generic request and response formats For simplicity, in this spec the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquely identify resources should be used. -#### Request +### Request ```typescript { @@ -156,9 +156,9 @@ For simplicity, in this spec the request and response GUID will be just `request } ``` -#### Response +### Response -Responses will be differentiated by the presence of a `responseGuid` field and MUST reference the requestGuid that they are responding to. +Responses will be differentiated by the presence of a `responseGuid` field and MUST reference the `requestGuid` that they are responding to. ```typescript { @@ -188,7 +188,7 @@ Responses will be differentiated by the presence of a `responseGuid` field and M DAs should send these messages on to the bridge, which will add the `source.desktopAgent` metadata. Further, when processing responses, the bridge should also augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. -### Individual message exchanges +## Individual message exchanges The sections below cover most scenarios for each of the Desktop Agent methods in order to explore how this protocol might work. @@ -201,7 +201,7 @@ Each section assumes that we have 3 agents connected by a bridge: ## Context -### For broadcasts on channels +### broadcast (on channels) Only needs a single message (no response). @@ -231,7 +231,7 @@ sequenceDiagram It encodes this as a message which it sends to the DAB ```JSON -// agent-a -> dab +// agent-A -> dab { "type": "broadcast", "payload": { @@ -251,11 +251,11 @@ It encodes this as a message which it sends to the DAB } ``` -which it repeats on to agent-B AND agent-c with the `source.desktopAgent` metadata added. +which it repeats on to agent-B AND agent-C with the `source.desktopAgent` metadata added. ```JSON -// dab -> agent-b -// dab -> agent-c +// dab -> agent-B +// dab -> agent-C { "type": "broadcast", "payload": { @@ -311,7 +311,7 @@ let appIntent = await fdc3.findIntent(); Sends an outward message to the DAB. ```JSON -// agent-a -> dab +// agent-A -> dab { "type": "findIntent", "payload": { @@ -331,11 +331,11 @@ Sends an outward message to the DAB. } ``` -The DAB fills in the `source.desktopAgent` field and forwards the request to the other desktop agents (agent-b AND agent-c). +The DAB fills in the `source.desktopAgent` field and forwards the request to the other desktop agents (agent-B AND agent-C). ```JSON -// dab -> agent-b -// dab -> agent-c +// dab -> agent-B +// dab -> agent-C { "type": "findIntent", "payload": { @@ -371,7 +371,7 @@ Normal response from agent-A, where the request was raised. } ``` -DA agent-b would produce response: +DA agent-B would produce response: ```JSON { @@ -388,7 +388,7 @@ DA agent-b would produce response: which is sent back over the bridge as a response to the request message as: ```JSON -// agent-b -> dab +// agent-B -> dab { "type": "findIntentResponse", "payload": { @@ -418,7 +418,7 @@ which is sent back over the bridge as a response to the request message as: } ``` -Note the response guid generated by the agent-b and the reference to the request guid produced by agent-a where the request was originated. +Note the response guid generated by the agent-B and the reference to the request guid produced by agent-A where the request was originated. This response gets repeated by the bridge in augmented form as: @@ -455,7 +455,7 @@ This response gets repeated by the bridge in augmented form as: } ``` -DA agent-c would produce response: +DA agent-C would produce response: ```JSON { @@ -469,7 +469,7 @@ DA agent-c would produce response: which is sent back over the bridge as a response to the request message as: ```JSON -// agent-c -> dab +// agent-C -> dab { "type": "findIntentResponse", "payload": { @@ -571,7 +571,6 @@ sequenceDiagram DAB ->>+ DA: intentResult ``` - #### Request format A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. @@ -583,7 +582,7 @@ raiseIntent(intent: string, context: Context): Promise; agent-A sends an outward `findIntent` message to the DAB: ```JSON -// agent-a -> dab +// agent-A -> dab { "type": "findIntent", "payload": { @@ -616,7 +615,7 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise dab +// agent-A -> dab { "type": "raiseIntent", "payload": { @@ -868,7 +867,7 @@ If intent result is private channel: The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// dab -> agent-b +// dab -> agent-B { "type": "privateChannelSubscribe", "payload": {}, @@ -1015,12 +1014,10 @@ The bridge will add in the source agent (agent-A) and forward the message to des } ``` ---- - -### `fdc3.open` +### open ```typescript - open(app: TargetApp, context?: Context): Promise; +open(app: TargetApp, context?: Context): Promise; ``` When receiving a response from invoking `fdc3.open` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppMetadata` with an `instanceId`. @@ -1069,7 +1066,6 @@ The second case is a little trickier as we don't know which agent may have the a * If you are a client * transmit the call to the server and await a response - ```mermaid sequenceDiagram participant DA as Desktop Agent A @@ -1152,10 +1148,10 @@ which is repeated as: } ``` -### `fdc3.findInstances` +### findInstances ```typescript - findInstances(app: TargetApp): Promise>; +findInstances(app: TargetApp): Promise>; ``` ```mermaid From 13aec1203b61a5a1dd646d8e7d0bd2b339d52f3b Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 3 May 2022 14:01:14 +0100 Subject: [PATCH 21/61] update --- docs/api-bridging/spec.md | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index bf4e1b26c..85d390203 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -197,7 +197,7 @@ Each section assumes that we have 3 agents connected by a bridge: * agent-A * agent-B * agent-C -* dab +* DAB ## Context @@ -231,7 +231,7 @@ sequenceDiagram It encodes this as a message which it sends to the DAB ```JSON -// agent-A -> dab +// agent-A -> DAB { "type": "broadcast", "payload": { @@ -254,8 +254,8 @@ It encodes this as a message which it sends to the DAB which it repeats on to agent-B AND agent-C with the `source.desktopAgent` metadata added. ```JSON -// dab -> agent-B -// dab -> agent-C +// DAB -> agent-B +// DAB -> agent-C { "type": "broadcast", "payload": { @@ -311,7 +311,7 @@ let appIntent = await fdc3.findIntent(); Sends an outward message to the DAB. ```JSON -// agent-A -> dab +// agent-A -> DAB { "type": "findIntent", "payload": { @@ -334,8 +334,8 @@ Sends an outward message to the DAB. The DAB fills in the `source.desktopAgent` field and forwards the request to the other desktop agents (agent-B AND agent-C). ```JSON -// dab -> agent-B -// dab -> agent-C +// DAB -> agent-B +// DAB -> agent-C { "type": "findIntent", "payload": { @@ -346,7 +346,7 @@ The DAB fills in the `source.desktopAgent` field and forwards the request to the "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { - "desktopAgent": "agent-A", // filled by dab + "desktopAgent": "agent-A", // filled by DAB "name": "", "appId": "", "version": "", @@ -388,7 +388,7 @@ DA agent-B would produce response: which is sent back over the bridge as a response to the request message as: ```JSON -// agent-B -> dab +// agent-B -> DAB { "type": "findIntentResponse", "payload": { @@ -469,7 +469,7 @@ DA agent-C would produce response: which is sent back over the bridge as a response to the request message as: ```JSON -// agent-C -> dab +// agent-C -> DAB { "type": "findIntentResponse", "payload": { @@ -531,7 +531,7 @@ This response gets repeated by the bridge in augmented form as: Then on agent-A the originating app finally gets back the following response from the bridge: ```JSON -// dab -> agent-A +// DAB -> agent-A { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ @@ -582,7 +582,7 @@ raiseIntent(intent: string, context: Context): Promise; agent-A sends an outward `findIntent` message to the DAB: ```JSON -// agent-A -> dab +// agent-A -> DAB { "type": "findIntent", "payload": { @@ -615,7 +615,7 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise dab +// agent-A -> DAB { "type": "raiseIntent", "payload": { @@ -648,7 +648,7 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise agent-B +// DAB -> agent-B { "type": "raiseIntent", "payload": { @@ -720,7 +720,7 @@ Normal response from agent-B, where the request was targeted to by agent-A. It s The bridge will fill in the `source.DesktopAgent` and relay the message on to agent-A ```JSON -// dab -> agent-A +// DAB -> agent-A { "type": "intentResolution", "payload": { @@ -838,7 +838,7 @@ If intent result is private channel: `onSubscribe` to the private channel sent to server: ```JSON -// agent-A -> dab +// agent-A -> DAB { "type": "privateChannelSubscribe", "payload": {}, @@ -867,7 +867,7 @@ If intent result is private channel: The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// dab -> agent-B +// DAB -> agent-B { "type": "privateChannelSubscribe", "payload": {}, @@ -898,7 +898,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des `onUnsubscribe` to the private channel sent to the bridge ```JSON -// agent-A -> dab +// agent-A -> DAB { "type": "privateChannelUnsubscribe", "payload": {}, @@ -927,7 +927,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// dab -> agent-B +// DAB -> agent-B { "requestGuid": "requestGuid", "timestamp": "2020-03-...", @@ -958,7 +958,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des `onDisconnect` to the private channel sent to the bridge ```JSON -// agent-A -> dab +// agent-A -> DAB { "type": "privateChannelDisconnect", "payload": {}, @@ -987,7 +987,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// dab -> agent-B +// DAB -> agent-B { "type": "privateChannelDisconnect", "payload": {}, From 409ac52159ceea8e17071822b68b0034650d3134 Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 4 May 2022 12:29:55 +0100 Subject: [PATCH 22/61] updating connection overview --- docs/api-bridging/spec.md | 52 +++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 85d390203..735f79c87 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,38 +1,52 @@ # Desktop Agent Bridging +The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent, facilitating cross-application workflows. Desktop Agent Bridging addresses the interconnection of desktop agents such that apps running under different desktop agents can also interoperate, allowing workflows to span multiple desktop agents. -In order to implement Desktop Agent Bridging some means for Desktop Agents to communicate with each other is needed. This spec assumes the Desktop Agent Bridging is implemented via a standalone bridge (instead of peer-to-peer or client/server topologies). Another assumption of this spec is that the data traffic will be over websocket connection. +In any desktop agent bridging scenario, it is expected that each Desktop Agent is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although Desktop Agents may be run on different machines. -This topology will be similar to a star topology on a network in which the Desktop Agent Bridge (DAB or simply bridge) will be the central node acting as a router. +## Connection Overview -The discovery, i.e. mechanism which allows us to discover which Desktop Agents (DA) are present in the "network" can be done via known port (TBD) or config. +### Topology +In order to implement Desktop Agent Bridging some means for desktop agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. -How the data will flow from Desktop Agent (DA) over a bridge to other DA will be outlined below. +Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemetned by desktop agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. -## Locating +Whilst the standalone bridge represents a single point of failure for the interconnection of desktop agents, it will also be significantly simpler than a full desktop agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then desktop agents will no longer be bridged and should act as single agents. -A DAB will implement a "server" behavior by: +### Technology & Service Discovery +Connections between desktop agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected desktop agent as a client. -* receiving requests from clients -* route requests to clients -* route responses to clients +The bridge SHOULD run on the same machine as the desktop agents, ensuring the websocket can be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. However, bridge implementations and desktop agents MAY support configuration of a specific alternative IP address to connect to. -A DA will implement a "client" behavior by: +Bridge implementations SHOULD default to binding their websocket server to port XXXX, enabling simple discovery of a running bridge via attepting a socket connection to that port and handshake (as defined later in this proposal). However, bridge implementations and desktop agents MAY support configuration of a specific alternative port number to connect to. -* forwarding requests to the bridge -* await response(s) from the bridge -* receive requests from the bridge +As part of the Desktop Agent Bridging protocol, a bridge will implement "server" behavior by: -## Connecting +* Receiving requests from client desktop agents. +* Routing requests to client desktop agents. +* Receiving responses from client desktop agents. +* Routing responses to client desktop agents. -Desktop Agents should authenticate against the bridge, which needs to implement the authentication logic (TBD - access keys? JWT?). +A desktop agent will implement "client" behavior by: -The DAB is also responsible for assigning each DA a name. a name can be requested by a DA. +* Forwarding requests to the bridge. +* Awaiting response(s) from the bridge. +* Receiving requests from the bridge. +* Forwarding response to the bridge. -Whilst the DAB represents a single point of failure in this bridging configuration, a critical failure should only mean that a DA will operate as if it was the only DA in a machine. +### Handshake, Authentication & Name Assignment +On connection to the bridge, a handshake and authentication step must be completed. This allows: +- The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. +- The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents +- The destkop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested namee, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. -## Interacting +The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. -With a standalone DAB, the message paths and message propagation should become simple to implement since messages will only from between a source and a destination with a DAB in the middle. A standalone DAB will also simplify the implementation for supporting multi-machine and Access Control Lists. + +## Interaction + + +With a standalone DAB, the message paths and message propagation should become simple to implement since messages will only from between a source and a destination with a DAB in the middle. A standalone DAB will also sim +plify the implementation for supporting multi-machine and Access Control Lists. ### Handling FDC3 calls When Bridged From e9e0bc6175393f3c2b81d94febaad8d3b724cf21 Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 4 May 2022 12:39:08 +0100 Subject: [PATCH 23/61] updating connection overview --- docs/api-bridging/spec.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 735f79c87..a7dc3803e 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -12,6 +12,9 @@ Other possible topologies include peer-to-peer or client/server networks, howeve Whilst the standalone bridge represents a single point of failure for the interconnection of desktop agents, it will also be significantly simpler than a full desktop agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then desktop agents will no longer be bridged and should act as single agents. +Where multi-machine use cases must be supported, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each desktop agent simply communicating with a bridge instance located on the same machine (or nominated IP address, where supported). Bridges running on different machines may then exchange messages between each other. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. + + ### Technology & Service Discovery Connections between desktop agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected desktop agent as a client. @@ -33,6 +36,8 @@ A desktop agent will implement "client" behavior by: * Receiving requests from the bridge. * Forwarding response to the bridge. +Hence, message paths and propagation are simple. All messages to other desktop agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. + ### Handshake, Authentication & Name Assignment On connection to the bridge, a handshake and authentication step must be completed. This allows: - The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. @@ -42,11 +47,7 @@ On connection to the bridge, a handshake and authentication step must be complet The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. -## Interaction - - -With a standalone DAB, the message paths and message propagation should become simple to implement since messages will only from between a source and a destination with a DAB in the middle. A standalone DAB will also sim -plify the implementation for supporting multi-machine and Access Control Lists. +## Interactions between Desktop Agents ### Handling FDC3 calls When Bridged From f7b647947d6e25b78ddbf676eb1b169ba436ae03 Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 4 May 2022 13:59:49 +0100 Subject: [PATCH 24/61] intro content and open questions --- docs/api-bridging/spec.md | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index c5ed0f4f9..466f4642f 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,7 +1,16 @@ # Desktop Agent Bridging The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent, facilitating cross-application workflows. Desktop Agent Bridging addresses the interconnection of desktop agents such that apps running under different desktop agents can also interoperate, allowing workflows to span multiple desktop agents. -In any desktop agent bridging scenario, it is expected that each Desktop Agent is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although Desktop Agents may be run on different machines. +In any desktop agent bridging scenario, it is expected that each Desktop Agent is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although Desktop Agents may be run on different machines operated by the same user. + +## Open questions / TODO list +* Add topology diagram +* Define handshake, authentication and naming protocol +* Do we need to make mandatory that a `fdc3.joinChannel` was invoked before you can return the current state of the channel? +* Bridging startup - Consider an app that joins a channel whose last context was sent before the bridge was created, then DA couldn’t send the correct initial context when the channel joined + * Current context is ignored + * Add an init flow where DAs share info about channels they have with context on them already - (this type of discovery of channels does not happen in FDC3 at the moment, therefore it would add significant complexity to the spec). +* How to handle slow responding DAs? ## Connection Overview @@ -48,10 +57,12 @@ The bridge is ultimately responsible for assigning each desktop agent a name and ## Interactions between Desktop Agents +The use of Desktop Agent Bridging affects how a desktop agent must handle FDC3 API calls. Details on how this affects the FD3 API and how a deskttop agent should interact with other agents over the bridge are provided below in this section. ### Handling FDC3 calls When Bridged -* DAs that are bridged will need to wait for responses from other DAs before responding to API calls. +#### WIP +Destkop Agents that are bridged will need to wait for responses from other DAs before responding to API calls. * for resilience, this may mean defining timeouts... ### Identifying Desktop Agents Identity and Message Sources @@ -86,14 +97,6 @@ The DAB MUST be able to forward messages received from one DA on to others (excl * If the message has a target Desktop Agent (e.g. response to findIntent) * The bridge will forward the message to it. -## Open Questions - -* Do we need to make mandatory that a `fdc3.joinChannel` was invoked before you can return the current state of the channel? -* Bridging startup - Consider an app that joins a channel whose last context was sent before the bridge was created, then DA couldn’t send the correct initial context when the channel joined - * Current context is ignored - * Add an init flow where DAs share info about channels they have with context on them already - (this type of discovery of channels does not happen in FDC3 at the moment, therefore it would add significant complexity to the spec). -* How to handle slow responding DAs? - ## AppMetadata `AppMetadata` needs to be expanded to contain a `desktopAgent` field. From 33b9bbbcbd105aec17813922009de26de1389721 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Fri, 6 May 2022 13:27:03 +0100 Subject: [PATCH 25/61] Typos Update from 04/05/2020 meeting --- docs/api-bridging/spec.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 466f4642f..51fa98963 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -17,7 +17,7 @@ In any desktop agent bridging scenario, it is expected that each Desktop Agent i ### Topology In order to implement Desktop Agent Bridging some means for desktop agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. -Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemetned by desktop agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. +Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by desktop agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. Whilst the standalone bridge represents a single point of failure for the interconnection of desktop agents, it will also be significantly simpler than a full desktop agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then desktop agents will no longer be bridged and should act as single agents. @@ -27,9 +27,11 @@ Where multi-machine use cases must be supported, cross-machine routing is an int ### Technology & Service Discovery Connections between desktop agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected desktop agent as a client. -The bridge SHOULD run on the same machine as the desktop agents, ensuring the websocket can be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. However, bridge implementations and desktop agents MAY support configuration of a specific alternative IP address to connect to. +The bridge MUST run on the same machine as the desktop agents, and the websocket MUST be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. -Bridge implementations SHOULD default to binding their websocket server to port XXXX, enabling simple discovery of a running bridge via attepting a socket connection to that port and handshake (as defined later in this proposal). However, bridge implementations and desktop agents MAY support configuration of a specific alternative port number to connect to. +Bridge implementations SHOULD default to binding the recommended websocket server to port XXXX, enabling simple discovery of a running bridge via attempting a socket connection to that port and handshake (as defined later in this proposal). However, bridge implementations and desktop agents SHOULD support configuration of a alternative port range which can be scanned for connection. + +Both DAs and the bridge MUST support configuration of the bridging port and/or port range. As part of the Desktop Agent Bridging protocol, a bridge will implement "server" behavior by: @@ -48,21 +50,25 @@ A desktop agent will implement "client" behavior by: Hence, message paths and propagation are simple. All messages to other desktop agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. ### Handshake, Authentication & Name Assignment -On connection to the bridge, a handshake and authentication step must be completed. This allows: +On connection to the bridge, a handshake and authentication step must be completed. This allows: + - The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. - The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents -- The destkop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested namee, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. +- The desktop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. +### Channels +It is assumed that Desktop Agents SHOULD adopt the recommended 8 channel set (and the respective display metadata). Desktop Agents MAY support channel customization through configuration. +The Desktop Agent Bridge MAY support channel mapping ability to deal with channel differences that can arise. ## Interactions between Desktop Agents -The use of Desktop Agent Bridging affects how a desktop agent must handle FDC3 API calls. Details on how this affects the FD3 API and how a deskttop agent should interact with other agents over the bridge are provided below in this section. +The use of Desktop Agent Bridging affects how a desktop agent must handle FDC3 API calls. Details on how this affects the FD3 API and how a desktop agent should interact with other agents over the bridge are provided below in this section. ### Handling FDC3 calls When Bridged #### WIP -Destkop Agents that are bridged will need to wait for responses from other DAs before responding to API calls. +Desktop Agents that are bridged will need to wait for responses from other DAs before responding to API calls. * for resilience, this may mean defining timeouts... ### Identifying Desktop Agents Identity and Message Sources @@ -1159,7 +1165,7 @@ which is repeated as: "requestGuid": "requestGuid", "timestamp": 2020-03-..., "source": { - "desktoAgent": "agent-A", // filled by DAB + "desktopAgent": "agent-A", // filled by DAB // ... other metadata fields } } From fdc09e7930cb62fbb0ebd30bdf2773dd64b86883 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Wed, 25 May 2022 17:49:01 +0100 Subject: [PATCH 26/61] Save --- docs/api-bridging/spec.md | 177 +++++++++++++++++++++----------------- 1 file changed, 100 insertions(+), 77 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 51fa98963..23e7df370 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,9 +1,11 @@ # Desktop Agent Bridging -The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent, facilitating cross-application workflows. Desktop Agent Bridging addresses the interconnection of desktop agents such that apps running under different desktop agents can also interoperate, allowing workflows to span multiple desktop agents. -In any desktop agent bridging scenario, it is expected that each Desktop Agent is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although Desktop Agents may be run on different machines operated by the same user. +The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent (DA), facilitating cross-application workflows. Desktop Agent Bridging addresses the interconnection of desktop agents (DAs) such that apps running under different desktop agents can also interoperate, allowing workflows to span multiple desktop agents. + +In any desktop agent bridging scenario, it is expected that each DA is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although DAs may be run on different machines operated by the same user. ## Open questions / TODO list + * Add topology diagram * Define handshake, authentication and naming protocol * Do we need to make mandatory that a `fdc3.joinChannel` was invoked before you can return the current state of the channel? @@ -15,23 +17,24 @@ In any desktop agent bridging scenario, it is expected that each Desktop Agent i ## Connection Overview ### Topology + In order to implement Desktop Agent Bridging some means for desktop agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by desktop agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. Whilst the standalone bridge represents a single point of failure for the interconnection of desktop agents, it will also be significantly simpler than a full desktop agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then desktop agents will no longer be bridged and should act as single agents. -Where multi-machine use cases must be supported, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each desktop agent simply communicating with a bridge instance located on the same machine (or nominated IP address, where supported). Bridges running on different machines may then exchange messages between each other. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. Further, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. - +In Financial services it is not unusual for a single user to be working with applications on more than one desktop. As desktop agents do not span desktops bridging desktop agents across multiple machines is an additional use case for desktop agent bridging. However, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. ### Technology & Service Discovery + Connections between desktop agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected desktop agent as a client. The bridge MUST run on the same machine as the desktop agents, and the websocket MUST be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. -Bridge implementations SHOULD default to binding the recommended websocket server to port XXXX, enabling simple discovery of a running bridge via attempting a socket connection to that port and handshake (as defined later in this proposal). However, bridge implementations and desktop agents SHOULD support configuration of a alternative port range which can be scanned for connection. +Bridge implementations SHOULD default to binding the websocket server to a port in the recommended port range XXXX - XXYY, enabling simple discovery of a running bridge via attempting socket connections to ports in that range and attempting a handshake (as defined later in this proposal) that will identify the websocket as belong to a Desktop Agent Bridge. A port range is used, in preference to a single nominated port, in order to enable the automatic resolution of port clashes with other services. -Both DAs and the bridge MUST support configuration of the bridging port and/or port range. +Both DAs and bridge implementations MUST support at least connection to the recommended port range and MAY also support configuration for connection to an alternative bridging port range. As part of the Desktop Agent Bridging protocol, a bridge will implement "server" behavior by: @@ -47,29 +50,69 @@ A desktop agent will implement "client" behavior by: * Receiving requests from the bridge. * Forwarding response to the bridge. -Hence, message paths and propagation are simple. All messages to other desktop agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. +Hence, message paths and propagation are simple. All messages to other desktop agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. -### Handshake, Authentication & Name Assignment -On connection to the bridge, a handshake and authentication step must be completed. This allows: +#### Bridging Desktop Agent on Multiple Machines -- The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. -- The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents -- The desktop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. +As the bridge binds its websocket on the loopback address (127.0.0.1) it cannot be connected to from another device. Hence, an instance of the standalone bridge may be run on each device and those instances exchange messages in order to implement the bridge cross-device. + +However, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each desktop agent simply communicating with a bridge instance located on the same machine. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. Further, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. However, methods of verifying the identity of user are currently beyond the scope of this Standard. + +### Connection Protocol + +On connection to the bridge handshake, authentication and name assignment steps must be completed. These allow: + +* The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. +* The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents +* The desktop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. +#### Step 1. Handshake + +TBC +Standard hello message that identifies that this is bridge, perhaps with implementation details for logging + +#### Step 2. Authentication + +Send in authentication details + a proposal for a specific name +TBC +Needs research + +#### Step 3. Auth Confirmation and Name Assignment + +TBC +Receive back auth confirmation and assigned name + Name to appear in ImplementationMetadata +Note: own name is rarely used by the desktop agent, but useful to log for debugging purposes + +#### Step 4. Connected agents update + +Message sent with names of other desktop agents connected to bridge +Message reissued whenever an agent connects or disconnects + Enables logging of connected agents and (optional) targeted API calls (e.g. `raiseIntent` to a specific agent) + ### Channels + It is assumed that Desktop Agents SHOULD adopt the recommended 8 channel set (and the respective display metadata). Desktop Agents MAY support channel customization through configuration. The Desktop Agent Bridge MAY support channel mapping ability to deal with channel differences that can arise. ## Interactions between Desktop Agents + The use of Desktop Agent Bridging affects how a desktop agent must handle FDC3 API calls. Details on how this affects the FD3 API and how a desktop agent should interact with other agents over the bridge are provided below in this section. ### Handling FDC3 calls When Bridged #### WIP -Desktop Agents that are bridged will need to wait for responses from other DAs before responding to API calls. - * for resilience, this may mean defining timeouts... + +Desktop Agents that are bridged will need to wait for responses from other DAs before responding to API calls. For resilience, this may mean defining timeouts. + +Different types of call + +* fire and forget, e.g. broadcast +* request/response + * send request, await responses + * who collates responses and implements timeouts (bridge or DA)? ### Identifying Desktop Agents Identity and Message Sources @@ -83,76 +126,61 @@ To prevent spoofing and to simplify the implementation of clients, sender identi * The DAB should do the assignments and could generate ids or accept them via config. * DAs don't need to know their own ids or even the ids of others, they just need to be able to pass around `AppMetadata` objects that contain them. -### Identifying Individual Messages - -There are a variety of message types we'll need to send between bridged DAs, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. - -* GUIDs required to uniquely identify messages - * To be referenced in replies - -This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response the does not reference a request GUID should be considered invalid. - -### Forwarding of Messages from Other Agents - -The DAB MUST be able to forward messages received from one DA on to others (excluding obviously the Desktop Agent where the request was originated). There are a few simple rules which determine whether a message needs to be forwarded: - -* the message does not have a target Desktop Agent (e.g. findIntent) - * If you are a DA, send it on to the bridge - * The bridge will send it on to other known DAs (except the source of the message) - -* If the message has a target Desktop Agent (e.g. response to findIntent) - * The bridge will forward the message to it. - -## AppMetadata +#### AppMetadata `AppMetadata` needs to be expanded to contain a `desktopAgent` field. ```typescript interface AppMetadata { - /** The unique app name that can be used with the open and raiseIntent calls. */ readonly name: string; - - /** The unique application identifier located within a specific application directory instance. An example of an appId might be 'app@sub.root' */ readonly appId?: string; - - /** The Version of the application. */ readonly version?: string; - - /** An optional instance identifier, indicating that this object represents a specific instance of the application described.*/ readonly instanceId?: string; - - /** An optional set of, implementation specific, metadata fields that can be used to disambiguate instances, such as a window title or screen position. Must only be set if `instanceId` is set. */ readonly instanceMetadata?: Record; - - /** A more user-friendly application title that can be used to render UI elements */ readonly title?: string; - - /** A tooltip for the application that can be used to render UI elements */ readonly tooltip?: string; - - /** A longer, multi-paragraph description for the application that could include mark-up */ readonly description?: string; - - /** A list of icon URLs for the application that can be used to render UI elements */ readonly icons?: Array; - - /** A list of image URLs for the application that can be used to render UI elements */ readonly images?: Array; - - /** The type of result returned for any intent specified during resolution. - * May express a particular context type (e.g. "fdc3.instrument"), channel - * (e.g. "channel") or a channel that will receive a specified type - * (e.g. "channel"). */ readonly resultType?: string | null; - /** A string filled in by server on receipt of message, that represents - * the Desktop Agent that the app is available on. */ + /** A string filled in by the DAB on receipt of a message, that represents + * the Desktop Agent that the app is available on. + **/ readonly desktopAgent?: string; } ``` +### Identifying Individual Messages + +There are a variety of message types we'll need to send between bridged DAs, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. + +* GUIDs required to uniquely identify messages + * To be referenced in replies + +This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response the does not reference a request GUID should be considered invalid. + +### Forwarding of Messages to Other Agents + +//TODO: rewrite based on DA deciding to send to bridge, bridge forwards on +//if target specified, forward to specific agent only + + +The DAB MUST be able to forward messages received from one DA on to others (excluding obviously the Desktop Agent where the request was originated). There are a few simple rules which determine whether a message needs to be forwarded: + +* the message does not have a target Desktop Agent (e.g. findIntent) + * If you are a DA, send it on to the bridge + * The bridge will send it on to other known DAs (except the source of the message) + +* If the message has a target Desktop Agent (e.g. response to findIntent) + * The bridge will forward the message to it. + + + ## Generic request and response formats +//TODO explain basic message structure type/payload (original FDC3 call args/response), meta (routing info). + For simplicity, in this spec the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquely identify resources should be used. ### Request @@ -1071,24 +1099,20 @@ The `fdc3.open` command should result in a single copy of the specified app bein The first case (target Desktop Agent is specified) is simple: * If the local Desktop Agent is the target, handle the call normally -* If you are a server - * check if any of your clients is the target and transmit the call to them and await a response - * If you are also a client of another server follow the client steps +* Otherwise: + * Request is sent to the bridge + * DAB checks to see if any of the connected DAs is the target and transmit the call to it and awaits a response * otherwise return `OpenError.AppNotFound` -* If you are a client - * transmit the call to the server and await a response The second case is a little trickier as we don't know which agent may have the app available: * If the local Desktop Agent has the app, open it and exit. -* If you are a server - * call each client one at a time and await a response - * If the response is `OpenError.AppNotFound` move to the next client - * If the response is `AppMetadata` then return it and exit - * If you are also a client of another server follow the client steps - * otherwise return `OpenError.AppNotFound` -* If you are a client - * transmit the call to the server and await a response +* Otherwise: + * Request is sent to the bridge + * Bridge will query each connected DA asynchronously and await a response + * If the response is `AppMetadata` then return it and exit (ignore every subsequent response) + * If the response is `OpenError.AppNotFound` and there are pending responses, wait for the next response + * If the response is `OpenError.AppNotFound` and there are NO pending responses, return `OpenError.AppNotFound` ```mermaid sequenceDiagram @@ -1099,9 +1123,8 @@ sequenceDiagram DA ->>+ DAB: Open Chart DAB ->>+ DB: Open Chart DAB ->>+ DC: Open Chart - DB ->> DB: App Found - DB ->> DB: Open App - DB -->>- DAB: Return App Data + DB ->> DB: App NOT Found + DB -->>- DAB: OpenError.AppNotFound DC ->> DC: App Found DC ->> DC: Open App DC -->>- DAB: Return App Data From 1c39fe78449d080659880f1cf25587a8e2f685e5 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 7 Jun 2022 14:19:27 +0100 Subject: [PATCH 27/61] Proposal update Co-authored-by: Kris West --- docs/api-bridging/spec.md | 203 +++++++++++++++++++++++++------------- 1 file changed, 136 insertions(+), 67 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 23e7df370..c632b267e 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -6,13 +6,39 @@ In any desktop agent bridging scenario, it is expected that each DA is being ope ## Open questions / TODO list -* Add topology diagram * Define handshake, authentication and naming protocol -* Do we need to make mandatory that a `fdc3.joinChannel` was invoked before you can return the current state of the channel? -* Bridging startup - Consider an app that joins a channel whose last context was sent before the bridge was created, then DA couldn’t send the correct initial context when the channel joined - * Current context is ignored - * Add an init flow where DAs share info about channels they have with context on them already - (this type of discovery of channels does not happen in FDC3 at the moment, therefore it would add significant complexity to the spec). -* How to handle slow responding DAs? + * Should auth be optional? E.g. triggered by a challenge? + +* How do we resolve differing state on channels (app/user/private) when a new DA joins the bridge? + * Problems + * DA joins the bridge on startup with no context on its channels, another DA already joined to the bridge has a different context on several channels + * DA joins the bridge after it has context on a channel, another DA already joined to the bridge has a different context on the same named channel + * N.B. current context is not just most recent context, but most recent of each type. + * App channels are not normally discoverable in the FDC3 API, what about under bridging? When should context on an app channel be shared with other agents? + * How do Private channels differ (as they are 'owned' by an app on a particular DA)? + * Solutions + * Assume bridge is up on start-up (system service) and that all DAs will connect to it as the first thing they do. + * ❌ Some DAs may retain state between sessions + * Ignore current context entirely, it will synchronize after one or messages have been sent. + * ⚠ ignores the problem, but it very simple to implement + * Get current context from other DAs when an app adds a context listener or calls `channel.getCurrentContext()` or `fdc3.joinChannel()` + * ❌ Which DA's state takes priority? No obvious answer + * Spreads out complexity throughout use, rather than getting it over with + * Add an init flow where DAs share info about channels they have with context on them already + * ✔️ Does actually solve the problem + * ⚠ Requires sharing of known channels and current state (this type of discovery of channels does not happen in the regular FDC3 API). + * ⚠ Requires state in the bridge. + * ⚠ Which DA's state takes priority? + * First to join bridge? Last to join bridge? use timestamps? + * Most recently sent context might be from the last to join... + * ⚠ Need to ensure agents join bridge serially so first/state of the first is known + +* Who collates responses to queries (e.g. findIntent, raiseIntent, findInstances etc.)? Bridge or requesting DA? + * How to handle slow responding DAs? Should there be a recommended timeout? + * Should an agent that is repeatedly timing out be disconnected? + * Should other agents report to users on connect/disconnect events? + +* Select recommended port range for bridge ## Connection Overview @@ -20,6 +46,19 @@ In any desktop agent bridging scenario, it is expected that each DA is being ope In order to implement Desktop Agent Bridging some means for desktop agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. +```mermaid +flowchart TD; + A[DA A] + B[DA B] + C[DA C] + D[DA D] + E{Bridge} + A <--> E + E <--> B + E <--> C + D <--> E +``` + Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by desktop agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. Whilst the standalone bridge represents a single point of failure for the interconnection of desktop agents, it will also be significantly simpler than a full desktop agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then desktop agents will no longer be bridged and should act as single agents. @@ -38,20 +77,26 @@ Both DAs and bridge implementations MUST support at least connection to the reco As part of the Desktop Agent Bridging protocol, a bridge will implement "server" behavior by: +* Accepting connections from client desktop agents, receiving and authenticating credentials and assigning a name (for routing purposes) * Receiving requests from client desktop agents. * Routing requests to client desktop agents. -* Receiving responses from client desktop agents. +* Receiving responses (and collating?) from client desktop agents. * Routing responses to client desktop agents. A desktop agent will implement "client" behavior by: +* Connecting to the bridge, providing authentication credentials and receiving an assigned named (for purposes) * Forwarding requests to the bridge. -* Awaiting response(s) from the bridge. +* Awaiting response(s) (and collating them?) from the bridge. * Receiving requests from the bridge. -* Forwarding response to the bridge. +* Sending responses to the bridge. Hence, message paths and propagation are simple. All messages to other desktop agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. +#### Collating responses + +TBD + #### Bridging Desktop Agent on Multiple Machines As the bridge binds its websocket on the loopback address (127.0.0.1) it cannot be connected to from another device. Hence, an instance of the standalone bridge may be run on each device and those instances exchange messages in order to implement the bridge cross-device. @@ -63,40 +108,74 @@ However, cross-machine routing is an internal concern of the Desktop Agent Bridg On connection to the bridge handshake, authentication and name assignment steps must be completed. These allow: * The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. -* The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents +* The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents. * The desktop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. - -The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. + * The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. Desktop agents MUST accept the name they are assigned by the bridge. #### Step 1. Handshake -TBC -Standard hello message that identifies that this is bridge, perhaps with implementation details for logging +Exchange standardized hello messages that identify: + +* That the server is a bridge, including: + * implementation details for logging by DA. + * supported FDC3 version(s). +* That the client is an FDC3 DA, including: + * implementation details (ImplementationMeta returned by fdc3.getInfo() call) for logging by DA and sharing with other DAs. + * already includes supported FDC3 version. + * request for a specific agent name. + +#### Step 2. Authentication (optional?) + +If auth is enabled: -#### Step 2. Authentication +* Bridge sends auth challenge +* DA must reply with credentials -Send in authentication details + a proposal for a specific name -TBC -Needs research +TBD: -#### Step 3. Auth Confirmation and Name Assignment +* What is the auth scheme? + * JWT token? or similar (return signature for some challenge data using private key) + * Bridge will need to be configured with credentials (public keys) for each agent that should be able to connect. + * Agents will need to be configured with credentials (private key). + * Preshared access token + * Simpler to configure, less secure + * Some form of SSO, e.g. OAuth + * Most complex to integrate + * Can confirm same user on each agent + * Integrated Windows auth? + * Platform specific, but could confirm same user on each agent -TBC -Receive back auth confirmation and assigned name - Name to appear in ImplementationMetadata -Note: own name is rarely used by the desktop agent, but useful to log for debugging purposes +If auth fails, server disconnects socket. +If auth succeeds move to next step. -#### Step 4. Connected agents update +#### Step 3. Auth Confirmation and Name Assignment -Message sent with names of other desktop agents connected to bridge -Message reissued whenever an agent connects or disconnects - Enables logging of connected agents and (optional) targeted API calls (e.g. `raiseIntent` to a specific agent) +TBD: -### Channels +* Receive back auth confirmation and assigned name + * Name to appear in ImplementationMetadata when retrieved locally through `fdc3.getInfo()` + * Note: own name is rarely used by the desktop agent, but useful to log for debugging purposes + +#### Step 4. Synchronize channel state + +TBD: + +* Details of the current state of channels (of DAs already connected to the bridge) may need to shared so that the incoming agent can have its state synchronized with the bridged group of agents. + +##### Channels It is assumed that Desktop Agents SHOULD adopt the recommended 8 channel set (and the respective display metadata). Desktop Agents MAY support channel customization through configuration. The Desktop Agent Bridge MAY support channel mapping ability to deal with channel differences that can arise. +#### Step 5. Connected agents update + +To enable logging of connected agents and (optional) targeted API calls (e.g. `raiseIntent` to a specific agent), the details of all agents connected to the bridge is shared with all other agents connected to the bridge. + +TBD: + +* Message sent with names of *all* desktop agents connected to bridge to *all* connected agents + * Message reissued whenever an agent connects or disconnects + ## Interactions between Desktop Agents The use of Desktop Agent Bridging affects how a desktop agent must handle FDC3 API calls. Details on how this affects the FD3 API and how a desktop agent should interact with other agents over the bridge are provided below in this section. @@ -118,33 +197,23 @@ Different types of call In order to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. -To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppMetadata` objects embedded in them. +To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppIdentifier` objects embedded in them. -* Sender details to be added by the DAB to the embedded `AppMetadata` objects. - * `AppMetadata` needs a new `desktopAgent` field +* Sender details to be added by the DAB to the embedded `AppIdentifier` objects. + * `AppIdentifier` needs a new `desktopAgent` field * When a client connects to a DAB it should be assigned an identity of some sort, which can be used to augment messages with details of the agent * The DAB should do the assignments and could generate ids or accept them via config. - * DAs don't need to know their own ids or even the ids of others, they just need to be able to pass around `AppMetadata` objects that contain them. + * DAs don't need to know their own ids or even the ids of others, they just need to be able to pass around `AppIdentifier` objects that contain them. -#### AppMetadata +#### AppIdentifier -`AppMetadata` needs to be expanded to contain a `desktopAgent` field. +`AppIdentifier` needs to be expanded to contain a `desktopAgent` field. ```typescript -interface AppMetadata { - readonly name: string; - readonly appId?: string; - readonly version?: string; +interface AppIdentifier { + readonly appId: string; readonly instanceId?: string; - readonly instanceMetadata?: Record; - readonly title?: string; - readonly tooltip?: string; - readonly description?: string; - readonly icons?: Array; - readonly images?: Array; - readonly resultType?: string | null; - - /** A string filled in by the DAB on receipt of a message, that represents + /** A string filled in by the bridge on receipt of a message, that represents * the Desktop Agent that the app is available on. **/ readonly desktopAgent?: string; @@ -202,8 +271,8 @@ For simplicity, in this spec the request and response GUID will be just `request requestGuid: string, /** Timestamp at which request was generated */ timestamp: date, - /** AppMetadata source request received from */ - source?: AppMetadata + /** AppIdentifier source request received from */ + source?: AppIdentifier } } ``` @@ -230,15 +299,15 @@ Responses will be differentiated by the presence of a `responseGuid` field and M responseGuid: string, /** Timestamp at which request was generated */ timestamp: Date, - /** AppMetadata source request received from */ - source?: AppMetadata, - /** AppMetadata destination response sent from */ - destination?: AppMetadata + /** AppIdentifier source request received from */ + source?: AppIdentifier, + /** AppIdentifier destination response sent from */ + destination?: AppIdentifier } } ``` -DAs should send these messages on to the bridge, which will add the `source.desktopAgent` metadata. Further, when processing responses, the bridge should also augment any `AppMetadata` objects in responses with the the same id applied to `source.desktopAgent`. +DAs should send these messages on to the bridge, which will add the `source.desktopAgent` metadata. Further, when processing responses, the bridge should also augment any `AppIdentifier` objects in responses with the the same id applied to `source.desktopAgent`. ## Individual message exchanges @@ -605,7 +674,7 @@ raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; +open(app: TargetApp, context?: Context): Promise; ``` -When receiving a response from invoking `fdc3.open` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppMetadata` with an `instanceId`. +When receiving a response from invoking `fdc3.open` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppIdentifier` with an `instanceId`. #### Request format @@ -1082,13 +1151,13 @@ A `fdc3.open` call is made on agent-A. // Open an app without context, using the app name let instanceMetadata = await fdc3.open('myApp'); -// Open an app without context, using an AppMetadata object to specify the target -let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; -let instanceMetadata = await fdc3.open(appMetadata); +// Open an app without context, using an AppIdentifier object to specify the target +let AppIdentifier = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; +let instanceMetadata = await fdc3.open(AppIdentifier); -// Open an app without context, using an AppMetadata object to specify the target and Desktop Agent -let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgent:"DesktopAgentB"}; -let instanceMetadata = await fdc3.open(appMetadata); +// Open an app without context, using an AppIdentifier object to specify the target and Desktop Agent +let AppIdentifier = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgent:"DesktopAgentB"}; +let instanceMetadata = await fdc3.open(AppIdentifier); ``` The `fdc3.open` command should result in a single copy of the specified app being opened and its instance data returned, or an error if it could not be opened. There are two possible scenarios: @@ -1110,7 +1179,7 @@ The second case is a little trickier as we don't know which agent may have the a * Otherwise: * Request is sent to the bridge * Bridge will query each connected DA asynchronously and await a response - * If the response is `AppMetadata` then return it and exit (ignore every subsequent response) + * If the response is `AppIdentifier` then return it and exit (ignore every subsequent response) * If the response is `OpenError.AppNotFound` and there are pending responses, wait for the next response * If the response is `OpenError.AppNotFound` and there are NO pending responses, return `OpenError.AppNotFound` @@ -1154,7 +1223,7 @@ It sends an outward message to the bridge: { "type": "open", "payload": { - "appMetaData": { + "AppIdentifier": { "name": "myApp", "appId": "myApp-v1.0.1", "version": "1.0.1", @@ -1176,7 +1245,7 @@ which is repeated as: { "type": "open", "payload": { - "appMetaData": { + "AppIdentifier": { "name": "myApp", "appId": "myApp-v1.0.1", "version": "1.0.1", @@ -1198,7 +1267,7 @@ which is repeated as: ### findInstances ```typescript -findInstances(app: TargetApp): Promise>; +findInstances(app: TargetApp): Promise>; ``` ```mermaid From 802ded086f97b7672427617d5ab1f0ff8dfff3cf Mon Sep 17 00:00:00 2001 From: Kris West Date: Fri, 10 Jun 2022 11:31:15 +0100 Subject: [PATCH 28/61] add bridging spec to website standard nav sidebar - note that diagrams do not render currently --- docs/api-bridging/spec.md | 8 ++++++-- website/sidebars.json | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index c632b267e..bca9b3a46 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1,6 +1,10 @@ -# Desktop Agent Bridging +--- +id: spec +sidebar_label: Overview +title: API Bridging Overview (next) +--- -The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent (DA), facilitating cross-application workflows. Desktop Agent Bridging addresses the interconnection of desktop agents (DAs) such that apps running under different desktop agents can also interoperate, allowing workflows to span multiple desktop agents. +The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent (DA), facilitating cross-application workflows. Desktop Agent API Bridging addresses the interconnection of desktop agents (DAs) such that apps running under different desktop agents can also interoperate, allowing workflows to span multiple desktop agents. In any desktop agent bridging scenario, it is expected that each DA is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although DAs may be run on different machines operated by the same user. diff --git a/website/sidebars.json b/website/sidebars.json index 4696470d6..f61e698e5 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -26,6 +26,13 @@ "api/ref/Errors" ] }, + { + "type": "subcategory", + "label": "API Bridging Part", + "ids": [ + "api-bridging/spec" + ] + }, { "type": "subcategory", "label": "App Directory Part", From 06ab23ce1831acf1fa03757d99c36855c94ff0e5 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Wed, 6 Jul 2022 12:28:47 +0100 Subject: [PATCH 29/61] Proposal update - connection protocol --- docs/api-bridging/spec.md | 266 +++++++++++++++++++++++++++----------- 1 file changed, 189 insertions(+), 77 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index c632b267e..e85acec82 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -6,37 +6,18 @@ In any desktop agent bridging scenario, it is expected that each DA is being ope ## Open questions / TODO list -* Define handshake, authentication and naming protocol +* Confirm draft handshake, authentication and naming protocol * Should auth be optional? E.g. triggered by a challenge? -* How do we resolve differing state on channels (app/user/private) when a new DA joins the bridge? - * Problems - * DA joins the bridge on startup with no context on its channels, another DA already joined to the bridge has a different context on several channels - * DA joins the bridge after it has context on a channel, another DA already joined to the bridge has a different context on the same named channel - * N.B. current context is not just most recent context, but most recent of each type. - * App channels are not normally discoverable in the FDC3 API, what about under bridging? When should context on an app channel be shared with other agents? - * How do Private channels differ (as they are 'owned' by an app on a particular DA)? - * Solutions - * Assume bridge is up on start-up (system service) and that all DAs will connect to it as the first thing they do. - * ❌ Some DAs may retain state between sessions - * Ignore current context entirely, it will synchronize after one or messages have been sent. - * ⚠ ignores the problem, but it very simple to implement - * Get current context from other DAs when an app adds a context listener or calls `channel.getCurrentContext()` or `fdc3.joinChannel()` - * ❌ Which DA's state takes priority? No obvious answer - * Spreads out complexity throughout use, rather than getting it over with - * Add an init flow where DAs share info about channels they have with context on them already - * ✔️ Does actually solve the problem - * ⚠ Requires sharing of known channels and current state (this type of discovery of channels does not happen in the regular FDC3 API). - * ⚠ Requires state in the bridge. - * ⚠ Which DA's state takes priority? - * First to join bridge? Last to join bridge? use timestamps? - * Most recently sent context might be from the last to join... - * ⚠ Need to ensure agents join bridge serially so first/state of the first is known - -* Who collates responses to queries (e.g. findIntent, raiseIntent, findInstances etc.)? Bridge or requesting DA? - * How to handle slow responding DAs? Should there be a recommended timeout? - * Should an agent that is repeatedly timing out be disconnected? - * Should other agents report to users on connect/disconnect events? +* Confirm method of resolving differing state on channels (app/user not private) when a new DA joins the bridge? + * Added an init flow where DAs share info about channels they have with context on them already + +* Define generic protocol for interacting with the bridge when handling fdc3 API calls + + * Include details of who collates responses to queries (e.g. findIntent, raiseIntent, findInstances etc.): The bridge + * Handle of slow responding DAs? Recommended timeout + * An agent that is repeatedly timing out should be disconnected + * Advise on whether other agents report to users on connect/disconnect events? * Select recommended port range for bridge @@ -53,10 +34,11 @@ flowchart TD; C[DA C] D[DA D] E{Bridge} - A <--> E - E <--> B - E <--> C - D <--> E + A <--> |websocket| E + E <--> |websocket| B + E <--> |websocket| C + D <--> |websocket| E + E --> |loopback - 127.0.0.1| E ``` Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by desktop agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. @@ -95,7 +77,21 @@ Hence, message paths and propagation are simple. All messages to other desktop a #### Collating responses -TBD +Whilst some FDC3 requests are fire and forget (e.g. broadcast) the main requests such as `findIntent` or `raiseIntent` expect a response. In a bridging scenario, the response can come from multiple Desktop Agents and therefore need to be aggregated and augmented before they are sent back to the requesting DA. + +The DAB is the responsible entity for collating responses together from all DAs. Whilst this approach may add some complexity to bridge implementations, it will simplify DA implementations since they only need to handle one response. + +The DAB MUST allow for timeout configuration. + +The Bridge SHOULD also implement timeout for waiting on DA responses. Assuming the message exchange will be all intra-machine, a recommended timeout of 2500ms/3000ms at most, should be implemented. + +#### Channels + +It is assumed that Desktop Agents SHOULD adopt the recommended 8 channel set (and the respective display metadata). Desktop Agents MAY support channel customization through configuration. + +The Desktop Agent Bridge MAY support channel mapping ability to deal with channel differences that can arise. + +A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync, which requires an initial synchronization step as part of the connection protocol. #### Bridging Desktop Agent on Multiple Machines @@ -105,18 +101,19 @@ However, cross-machine routing is an internal concern of the Desktop Agent Bridg ### Connection Protocol -On connection to the bridge handshake, authentication and name assignment steps must be completed. These allow: +On connection to the bridge's websocket, a handshake must be completed that may include an authentication step before a name is assigned to the desktop agent for use in routing messages. The purpose of the handshake is to allow: * The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. * The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents. * The desktop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. - * The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. Desktop agents MUST accept the name they are assigned by the bridge. + +The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. Desktop agents MUST accept the name they are assigned by the bridge. #### Step 1. Handshake Exchange standardized hello messages that identify: -* That the server is a bridge, including: +* That the server is a bridge, including: * implementation details for logging by DA. * supported FDC3 version(s). * That the client is an FDC3 DA, including: @@ -124,57 +121,180 @@ Exchange standardized hello messages that identify: * already includes supported FDC3 version. * request for a specific agent name. +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DAB ->>+ DA: hello + DA ->>+ DAB: handshake + DAB ->>+ DA: connectedAgentsUpdate + DAB ->>+ DB: connectedAgentsUpdate + DAB ->>+ DC: connectedAgentsUpdate + +``` + +When a new connection is made to the DAB websocket, it sends a `hello` message, including its metadata. + +```typescript +{ + type: "hello", + payload: { + desktopAgentBridgeVersion: number, + supportedFDC3Versions: string[], + authRequired: boolean + } + meta: { + timestamp: date + } +} +``` + +A Desktop Agent can use the structure of this message to determine that it has connected to a Desktop Agent Bridge (i.e by checking `msg.type === hello && msg.payload.desktopAgentBridgeVersion`), whether it supports a compatible FDC3 version and whether it is expected to provide authentication credentials in the next step (`if(msg.payload.authRequired) { ... }`). + +DA should respond to the `hello` message with a `handshake` request to the bridge, including an auth token (JWT) if required. + +```typescript +{ + type: "handshake", + /** Request body, containing the arguments to the function called.*/ + payload: { + /** The JWT authentication token */ + authToken?: string + /** DesktopAgent implementationMetadata trying to connect to the bridge */ + implementationMetadata: ImplementationMetadata, + /** The requested DA name */ + requestedName: string, + /** The current state of the Desktop Agent's channels, excluding any private channels, + * as a mapping of channel id to an array of Context objects, most recent first.*/ + channelsState: Record{ + }, + meta: { + /** Unique GUID for this request */ + requestGuid: string, + /** Timestamp at which request was generated */ + timestamp: date + } +} +``` + #### Step 2. Authentication (optional?) -If auth is enabled: +If requested by the server, the JWT auth token payload should take the form: -* Bridge sends auth challenge -* DA must reply with credentials +```typescript +{ + "sub": string, // UUID for the keypair used to sign the token + "iat": date // timestamp at which the the token was generated as specified in ISO 8601 +} +``` -TBD: +e.g. +```JSON +{ + "sub": "65141135-7200-47d3-9777-eb8786dd31c7", + "iat": "2022-07-06T10:11:43.492Z" +} +``` -* What is the auth scheme? - * JWT token? or similar (return signature for some challenge data using private key) - * Bridge will need to be configured with credentials (public keys) for each agent that should be able to connect. - * Agents will need to be configured with credentials (private key). - * Preshared access token - * Simpler to configure, less secure - * Some form of SSO, e.g. OAuth - * Most complex to integrate - * Can confirm same user on each agent - * Integrated Windows auth? - * Platform specific, but could confirm same user on each agent +Note that the `sub` SHOULD be a UUID that does NOT need to match the name requested by the Desktop Agent. It will be used to identify the keypair that should be used to validate the JWT token. Further, multiple Desktop Agent's MAY share the same keys for authentication and hence the same `sub`, but they will be assigned different names for routing purposes by the DAB. If an agent disconnects from the bridge and later re-connects it MAY request and be assigned the same name it connected with before. -If auth fails, server disconnects socket. -If auth succeeds move to next step. +The DAB will extract the authentication token `sub` from the JWT token's claims and then verify the token's signature against any public key it has been configured with. If the signature can't be verified, the bridge should respond with the below authentication failed message and the socket should be disconnected by the bridge. + +```typescript +{ + type: "authenticationFailed", + meta: { + /** Timestamp at which response was generated */ + timestamp: date + /** GUID for the handshake request */ + requestGuid: string, + /** Unique guid for this message */ + responseGuid: string, + } +} +``` #### Step 3. Auth Confirmation and Name Assignment -TBD: +If authentication succeeds (or is not required), then the DAB should assign the Desktop Agent, and associated socket connection, the name requested in the `handshake` message, unless another agent is already connected with that name in which case it should generate a new name which MAY be derived from the requested name. -* Receive back auth confirmation and assigned name - * Name to appear in ImplementationMetadata when retrieved locally through `fdc3.getInfo()` - * Note: own name is rarely used by the desktop agent, but useful to log for debugging purposes +A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so the states must be synchronized whenever a new agent connects. Hence, the Bridge must process the `channelState` provided by the new agent in the `handshake` request containing details of each known User Channel or App Channel and its state, compare it to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to connected agents to ensure that they are synchronized with it. -#### Step 4. Synchronize channel state +#### Step 4. Synchronize the bridge's channel state -TBD: +Channel state of agents's already connected to the bridge should take precedence over agents that are connecting. However, if all agent's disconnect from the bridge it should reset (clear) its internal state and adopt that of the first agent to connect. The state of any Private channels does not need to be included as these can only be retrieved via a raised intent. -* Details of the current state of channels (of DAs already connected to the bridge) may need to shared so that the incoming agent can have its state synchronized with the bridged group of agents. +When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast of context as it may result in an older context, of a type not already found on the channel, being merged into the channel (and subsequently broadcast to context listeners with a matching type). -##### Channels +The incoming `channelsState` should be merged with the `existingChannelsState` as follows: -It is assumed that Desktop Agents SHOULD adopt the recommended 8 channel set (and the respective display metadata). Desktop Agents MAY support channel customization through configuration. -The Desktop Agent Bridge MAY support channel mapping ability to deal with channel differences that can arise. +```typescript +Object.keys(channelsState).forEach((channelId) => { + if (!existingChannelsState[channelId]) { + //unknown channel, just aodopt its state + existingChannelsState[channelId] = channelsState[channelId]; + } else { + //known channel merge state, with existing state taking precedence + const currentState = existingChannelsState[channelId]; + const incoming = channelsState[channelId]; + incoming.forEach((context) => { + if (!currentState.find(element => element.type === context.type)){ + currentState.push(context); + } + // ignore any types that are already known and + // preserve most recent channel state by adding to the end of the array + }); + } +}); +``` #### Step 5. Connected agents update -To enable logging of connected agents and (optional) targeted API calls (e.g. `raiseIntent` to a specific agent), the details of all agents connected to the bridge is shared with all other agents connected to the bridge. +The updated `existingChannelsState` will then be shared with all connected agents along with updated details of all connected agents via a `connectedAgentsUpdate` message sent to all connected sockets. The newly connected agent will receive both its assigned name and channel state via this message. The `connectedAgentsUpdate` message will be linked to the handshake request by quoting the `meta.requestGuid` of the `handshake` message. + +The `connectedAgentsUpdate` message will take the form: + +```typescript +{ + type: "connectedAgentsUpdate", + /** Request body, containing the arguments to the function called.*/ + payload: { + /** Should be set when an agent first connects to the bridge and provide its assigned name. */ + addAgent?: string, + /** Should be set when an agent disconnects from the bridge and provide the name that no longer is assigned. */ + removeAgent?: string, + /** Desktop Agent Bridge implementation metadata of all connected agents. + * Note that this object is extended to include a `desktopAgent` field with the name assigned by the DAB. */ + allAgents: ImplementationMetadata[], + /** The updated state of channels that should be adopted by the agents. SHOULD only be set when an agent is connecting to the bridge. */ + channelsState?: ChannelState[] // see step4 + }, + meta: { + /** For a new connection, should be the same as the handshake requestGuid. + * Should be the same as the responseGuid for a disconnection. + */ + requestGuid: string, + /** Unique guid for this message */ + responseGuid: string, + /** Timestamp at which response was generated */ + timestamp: date, + } +} +``` + +When applying the updated channel state, it should be noted that desktop agents will not have context listeners for previously unknown channels and can simply record that channel's state for use when that channel is first used. For known channel names, the Desktop Agent must also compare its current state to that which it has just received and may need to broadcast context to existing connected listeners. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as the most recently transmitted Context should be received by un-typed listeners). Hence, updating listeners for a known channel should be performed as follows: + +1. The incoming channel state `channelState` for a particular channel should be processed from last (oldest) to first. +2. If there is no current context of that type, broadcast it to listeners of that specific type only. +3. If there is a current context of that type, and it does not match the incoming object exactly, broadcast it to listeners of that specific type only. +4. If the most recent (first in the incoming array) type OR value of that type doesn't match the most recent context broadcast on the channel, broadcast it to un-typed listeners only. + +After applying the `connectedAgentsUpdate` message, the newly connected desktop agent and other already connected agents are able to begin communicating through the bridge. + -TBD: -* Message sent with names of *all* desktop agents connected to bridge to *all* connected agents - * Message reissued whenever an agent connects or disconnects ## Interactions between Desktop Agents @@ -182,16 +302,8 @@ The use of Desktop Agent Bridging affects how a desktop agent must handle FDC3 A ### Handling FDC3 calls When Bridged -#### WIP - -Desktop Agents that are bridged will need to wait for responses from other DAs before responding to API calls. For resilience, this may mean defining timeouts. - -Different types of call +TBC - describe generic protocol for working with the bridge for both fire and forget and request/response type calls. -* fire and forget, e.g. broadcast -* request/response - * send request, await responses - * who collates responses and implements timeouts (bridge or DA)? ### Identifying Desktop Agents Identity and Message Sources From 4e2a809ca52a3dc9c17d77d0c5dd7d2e5744be4b Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 6 Jul 2022 12:40:57 +0100 Subject: [PATCH 30/61] Update docs/api-bridging/spec.md --- docs/api-bridging/spec.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index e85acec82..b613ee79c 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -293,6 +293,9 @@ When applying the updated channel state, it should be noted that desktop agents After applying the `connectedAgentsUpdate` message, the newly connected desktop agent and other already connected agents are able to begin communicating through the bridge. +#### Step 6. Disconnects + +Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 5 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. From 471f90b3d57b678266de33f761a5fbcda02efece Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Wed, 6 Jul 2022 16:23:57 +0100 Subject: [PATCH 31/61] Typos spotted during discussion group session --- docs/api-bridging/spec.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index b613ee79c..d48953b34 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -144,7 +144,7 @@ When a new connection is made to the DAB websocket, it sends a `hello` message, desktopAgentBridgeVersion: number, supportedFDC3Versions: string[], authRequired: boolean - } + }, meta: { timestamp: date } @@ -161,14 +161,14 @@ DA should respond to the `hello` message with a `handshake` request to the bridg /** Request body, containing the arguments to the function called.*/ payload: { /** The JWT authentication token */ - authToken?: string + authToken?: string, /** DesktopAgent implementationMetadata trying to connect to the bridge */ implementationMetadata: ImplementationMetadata, /** The requested DA name */ requestedName: string, /** The current state of the Desktop Agent's channels, excluding any private channels, * as a mapping of channel id to an array of Context objects, most recent first.*/ - channelsState: Record{ + channelsState: Record }, meta: { /** Unique GUID for this request */ @@ -200,6 +200,8 @@ e.g. Note that the `sub` SHOULD be a UUID that does NOT need to match the name requested by the Desktop Agent. It will be used to identify the keypair that should be used to validate the JWT token. Further, multiple Desktop Agent's MAY share the same keys for authentication and hence the same `sub`, but they will be assigned different names for routing purposes by the DAB. If an agent disconnects from the bridge and later re-connects it MAY request and be assigned the same name it connected with before. +#### Step 3. Auth Confirmation and Name Assignment + The DAB will extract the authentication token `sub` from the JWT token's claims and then verify the token's signature against any public key it has been configured with. If the signature can't be verified, the bridge should respond with the below authentication failed message and the socket should be disconnected by the bridge. ```typescript @@ -207,7 +209,7 @@ The DAB will extract the authentication token `sub` from the JWT token's claims type: "authenticationFailed", meta: { /** Timestamp at which response was generated */ - timestamp: date + timestamp: date, /** GUID for the handshake request */ requestGuid: string, /** Unique guid for this message */ @@ -216,13 +218,12 @@ The DAB will extract the authentication token `sub` from the JWT token's claims } ``` -#### Step 3. Auth Confirmation and Name Assignment - If authentication succeeds (or is not required), then the DAB should assign the Desktop Agent, and associated socket connection, the name requested in the `handshake` message, unless another agent is already connected with that name in which case it should generate a new name which MAY be derived from the requested name. +#### Step 4. Synchronize the bridge's channel state + A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so the states must be synchronized whenever a new agent connects. Hence, the Bridge must process the `channelState` provided by the new agent in the `handshake` request containing details of each known User Channel or App Channel and its state, compare it to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to connected agents to ensure that they are synchronized with it. -#### Step 4. Synchronize the bridge's channel state Channel state of agents's already connected to the bridge should take precedence over agents that are connecting. However, if all agent's disconnect from the bridge it should reset (clear) its internal state and adopt that of the first agent to connect. The state of any Private channels does not need to be included as these can only be retrieved via a raised intent. From 57d8ba98fe58d7bb851255cc9634b4923868696a Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Mon, 25 Jul 2022 16:28:53 +0100 Subject: [PATCH 32/61] typos and clarifications to spec --- docs/api-bridging/spec.md | 47 +++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index d48953b34..f41ca4edb 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -6,20 +6,21 @@ In any desktop agent bridging scenario, it is expected that each DA is being ope ## Open questions / TODO list -* Confirm draft handshake, authentication and naming protocol - * Should auth be optional? E.g. triggered by a challenge? - -* Confirm method of resolving differing state on channels (app/user not private) when a new DA joins the bridge? - * Added an init flow where DAs share info about channels they have with context on them already - +* Handshake + * Confirm we want to add optional auth token to `hello` message coming from the bridge for DA to authenticate the bridge? (Two-way authentication) +* Channel State Synchronization + * Add details about potential race conditions or workflows broken by disconnection + * Handling race conditions - e.g. on startup apps might load and start broadcasting state immediately - concurrent with the desktop agent connecting to the bridge and receiving state + * Handle handshakes sequentially one at a time + * Atomic operations for broadcasts and channel synchronization messages + * Other suggestions? * Define generic protocol for interacting with the bridge when handling fdc3 API calls - - * Include details of who collates responses to queries (e.g. findIntent, raiseIntent, findInstances etc.): The bridge - * Handle of slow responding DAs? Recommended timeout - * An agent that is repeatedly timing out should be disconnected - * Advise on whether other agents report to users on connect/disconnect events? + * Handle of slow responding DAs? + * Recommended timeout of 2500ms/3000ms + * An agent that is repeatedly timing out should be disconnected? + * Advise on whether other agents report to users on connect/disconnect events? +* Confirm recommended port range for bridge - (4475 - 4575) -* Select recommended port range for bridge ## Connection Overview @@ -53,7 +54,7 @@ Connections between desktop agents and the Desktop Agent Bridge will be made via The bridge MUST run on the same machine as the desktop agents, and the websocket MUST be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. -Bridge implementations SHOULD default to binding the websocket server to a port in the recommended port range XXXX - XXYY, enabling simple discovery of a running bridge via attempting socket connections to ports in that range and attempting a handshake (as defined later in this proposal) that will identify the websocket as belong to a Desktop Agent Bridge. A port range is used, in preference to a single nominated port, in order to enable the automatic resolution of port clashes with other services. +Bridge implementations SHOULD default to binding the websocket server to a port in the recommended port range 4475 - 4575, enabling simple discovery of a running bridge via attempting socket connections to ports in that range and attempting a handshake (as defined later in this proposal) that will identify the websocket as belong to a Desktop Agent Bridge. A port range is used, in preference to a single nominated port, in order to enable the automatic resolution of port clashes with other services. Both DAs and bridge implementations MUST support at least connection to the recommended port range and MAY also support configuration for connection to an alternative bridging port range. @@ -127,6 +128,7 @@ sequenceDiagram participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C + DA ->>+ DAB: connect DAB ->>+ DA: hello DA ->>+ DAB: handshake DAB ->>+ DA: connectedAgentsUpdate @@ -143,7 +145,9 @@ When a new connection is made to the DAB websocket, it sends a `hello` message, payload: { desktopAgentBridgeVersion: number, supportedFDC3Versions: string[], - authRequired: boolean + authRequired: boolean, + /** The DAB JWT authentication token */ + authToken?: string }, meta: { timestamp: date @@ -151,7 +155,9 @@ When a new connection is made to the DAB websocket, it sends a `hello` message, } ``` -A Desktop Agent can use the structure of this message to determine that it has connected to a Desktop Agent Bridge (i.e by checking `msg.type === hello && msg.payload.desktopAgentBridgeVersion`), whether it supports a compatible FDC3 version and whether it is expected to provide authentication credentials in the next step (`if(msg.payload.authRequired) { ... }`). +A Desktop Agent can use the structure of this message to determine that it has connected to a Desktop Agent Bridge (i.e by checking `msg.type === "hello" && msg.payload.desktopAgentBridgeVersion`), whether it supports a compatible FDC3 version and whether it is expected to provide authentication credentials in the next step (`if(msg.payload.authRequired) { ... }`). + +An optional JWT token can be included in the `hello` message to allow the connecting agent to authenticate the bridge. Verification of the JWM by the DA is option but recommended, meaning that the DA SHOULD verify the received JWT when one is included in the `hello` message. DA should respond to the `hello` message with a `handshake` request to the bridge, including an auth token (JWT) if required. @@ -222,12 +228,11 @@ If authentication succeeds (or is not required), then the DAB should assign the #### Step 4. Synchronize the bridge's channel state -A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so the states must be synchronized whenever a new agent connects. Hence, the Bridge must process the `channelState` provided by the new agent in the `handshake` request containing details of each known User Channel or App Channel and its state, compare it to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to connected agents to ensure that they are synchronized with it. - +Channels are the only stateful mechanism in the FDC3 that we have to consider. A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so, the states must be synchronized whenever a new agent connects. Hence, the Bridge must process the `channelState` provided by the new agent in the `handshake` request containing details of each known User Channel or App Channel and its state, compare it to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to connected agents to ensure that they are synchronized with it. Channel state of agents's already connected to the bridge should take precedence over agents that are connecting. However, if all agent's disconnect from the bridge it should reset (clear) its internal state and adopt that of the first agent to connect. The state of any Private channels does not need to be included as these can only be retrieved via a raised intent. -When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast of context as it may result in an older context, of a type not already found on the channel, being merged into the channel (and subsequently broadcast to context listeners with a matching type). +When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast of context as it may result in an older context, of a type not already found on the channel being merged into the channel (and subsequently broadcast to context listeners with a matching type). The incoming `channelsState` should be merged with the `existingChannelsState` as follows: @@ -251,6 +256,8 @@ Object.keys(channelsState).forEach((channelId) => { }); ``` +For the scenario of multiple agents connecting to the Desktop Agent Bridge at the same time, this portion of the handshake should be handled by the DAB serially to ensure correct channel state synchronization. + #### Step 5. Connected agents update The updated `existingChannelsState` will then be shared with all connected agents along with updated details of all connected agents via a `connectedAgentsUpdate` message sent to all connected sockets. The newly connected agent will receive both its assigned name and channel state via this message. The `connectedAgentsUpdate` message will be linked to the handshake request by quoting the `meta.requestGuid` of the `handshake` message. @@ -285,7 +292,7 @@ The `connectedAgentsUpdate` message will take the form: } ``` -When applying the updated channel state, it should be noted that desktop agents will not have context listeners for previously unknown channels and can simply record that channel's state for use when that channel is first used. For known channel names, the Desktop Agent must also compare its current state to that which it has just received and may need to broadcast context to existing connected listeners. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as the most recently transmitted Context should be received by un-typed listeners). Hence, updating listeners for a known channel should be performed as follows: +When applying the updated channel state, it should be noted that desktop agents will not have context listeners for previously unknown channels, and can simply record that channel's state for use when that channel is first used. For known channel names, the Desktop Agent must also compare its current state to that which it has just received and may need to broadcast context to existing connected listeners. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as the most recently transmitted Context should be received by un-typed listeners). Hence, updating listeners for a known channel should be performed as follows: 1. The incoming channel state `channelState` for a particular channel should be processed from last (oldest) to first. 2. If there is no current context of that type, broadcast it to listeners of that specific type only. @@ -294,6 +301,8 @@ When applying the updated channel state, it should be noted that desktop agents After applying the `connectedAgentsUpdate` message, the newly connected desktop agent and other already connected agents are able to begin communicating through the bridge. +Channel state synchronization messages from the DAB to Desktop Agents should be handled atomically to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. + #### Step 6. Disconnects Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 5 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. From c4b1358a7a5ff63736981aac6d7a6c66dc877253 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 26 Jul 2022 12:17:24 +0100 Subject: [PATCH 33/61] Proposal update Co-authored-by: Kris West --- docs/api-bridging/spec.md | 160 ++++++++++++++++++++------------------ 1 file changed, 86 insertions(+), 74 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 7500c1ed0..d40af3c7f 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -4,9 +4,9 @@ sidebar_label: Overview title: API Bridging Overview (next) --- -The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent (DA), facilitating cross-application workflows. Desktop Agent API Bridging addresses the interconnection of desktop agents (DAs) such that apps running under different desktop agents can also interoperate, allowing workflows to span multiple desktop agents. +The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent (DA), facilitating cross-application workflows. Desktop Agent API Bridging addresses the interconnection of Deskop Agents (DAs) such that apps running under different Deskop Agents can also interoperate, allowing workflows to span multiple Deskop Agents. -In any desktop agent bridging scenario, it is expected that each DA is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although DAs may be run on different machines operated by the same user. +In any Deskop Agent bridging scenario, it is expected that each DA is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although DAs may be run on different machines operated by the same user. ## Open questions / TODO list @@ -14,23 +14,26 @@ In any desktop agent bridging scenario, it is expected that each DA is being ope * Confirm we want to add optional auth token to `hello` message coming from the bridge for DA to authenticate the bridge? (Two-way authentication) * Channel State Synchronization * Add details about potential race conditions or workflows broken by disconnection - * Handling race conditions - e.g. on startup apps might load and start broadcasting state immediately - concurrent with the desktop agent connecting to the bridge and receiving state + * Handling race conditions - e.g. on startup apps might load and start broadcasting state immediately - concurrent with the Deskop Agent connecting to the bridge and receiving state * Handle handshakes sequentially one at a time * Atomic operations for broadcasts and channel synchronization messages * Other suggestions? +* Confirm recommended port range for bridge - (4470 - 4480) * Define generic protocol for interacting with the bridge when handling fdc3 API calls * Handle of slow responding DAs? * Recommended timeout of 2500ms/3000ms * An agent that is repeatedly timing out should be disconnected? * Advise on whether other agents report to users on connect/disconnect events? -* Confirm recommended port range for bridge - (4475 - 4575) +* Add section on GUID generation (with reference to external standard) used in requestGuid and response Guid fields in messages. +* Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction +* Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to references page ## Connection Overview ### Topology -In order to implement Desktop Agent Bridging some means for desktop agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. +In order to implement Desktop Agent Bridging some means for Deskop Agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. ```mermaid flowchart TD; @@ -46,17 +49,17 @@ flowchart TD; E --> |loopback - 127.0.0.1| E ``` -Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by desktop agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. +Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by Deskop Agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. -Whilst the standalone bridge represents a single point of failure for the interconnection of desktop agents, it will also be significantly simpler than a full desktop agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then desktop agents will no longer be bridged and should act as single agents. +Whilst the standalone bridge represents a single point of failure for the interconnection of Deskop Agents, it will also be significantly simpler than a full Deskop Agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then Deskop Agents will no longer be bridged and should act as single agents. -In Financial services it is not unusual for a single user to be working with applications on more than one desktop. As desktop agents do not span desktops bridging desktop agents across multiple machines is an additional use case for desktop agent bridging. However, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. +In Financial services it is not unusual for a single user to be working with applications on more than one desktop. As Deskop Agents do not span desktops bridging Deskop Agents across multiple machines is an additional use case for Deskop Agent bridging. However, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. ### Technology & Service Discovery -Connections between desktop agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected desktop agent as a client. +Connections between Deskop Agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected Deskop Agent as a client. -The bridge MUST run on the same machine as the desktop agents, and the websocket MUST be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. +The bridge MUST run on the same machine as the Deskop Agents, and the websocket MUST be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. Bridge implementations SHOULD default to binding the websocket server to a port in the recommended port range 4475 - 4575, enabling simple discovery of a running bridge via attempting socket connections to ports in that range and attempting a handshake (as defined later in this proposal) that will identify the websocket as belong to a Desktop Agent Bridge. A port range is used, in preference to a single nominated port, in order to enable the automatic resolution of port clashes with other services. @@ -64,13 +67,13 @@ Both DAs and bridge implementations MUST support at least connection to the reco As part of the Desktop Agent Bridging protocol, a bridge will implement "server" behavior by: -* Accepting connections from client desktop agents, receiving and authenticating credentials and assigning a name (for routing purposes) -* Receiving requests from client desktop agents. -* Routing requests to client desktop agents. -* Receiving responses (and collating?) from client desktop agents. -* Routing responses to client desktop agents. +* Accepting connections from client Deskop Agents, receiving and authenticating credentials and assigning a name (for routing purposes) +* Receiving requests from client Deskop Agents. +* Routing requests to client Deskop Agents. +* Receiving responses (and collating?) from client Deskop Agents. +* Routing responses to client Deskop Agents. -A desktop agent will implement "client" behavior by: +A Deskop Agent will implement "client" behavior by: * Connecting to the bridge, providing authentication credentials and receiving an assigned named (for purposes) * Forwarding requests to the bridge. @@ -78,7 +81,7 @@ A desktop agent will implement "client" behavior by: * Receiving requests from the bridge. * Sending responses to the bridge. -Hence, message paths and propagation are simple. All messages to other desktop agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. +Hence, message paths and propagation are simple. All messages to other Deskop Agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. #### Collating responses @@ -102,21 +105,19 @@ A key responsibility of the DAB is ensuring that the channel state of the connec As the bridge binds its websocket on the loopback address (127.0.0.1) it cannot be connected to from another device. Hence, an instance of the standalone bridge may be run on each device and those instances exchange messages in order to implement the bridge cross-device. -However, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each desktop agent simply communicating with a bridge instance located on the same machine. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. Further, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. However, methods of verifying the identity of user are currently beyond the scope of this Standard. +However, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each Deskop Agent simply communicating with a bridge instance located on the same machine. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. Further, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. However, methods of verifying the identity of user are currently beyond the scope of this Standard. ### Connection Protocol -On connection to the bridge's websocket, a handshake must be completed that may include an authentication step before a name is assigned to the desktop agent for use in routing messages. The purpose of the handshake is to allow: +On connection to the bridge's websocket, a handshake must be completed that may include an authentication step before a name is assigned to the Deskop Agent for use in routing messages. The purpose of the handshake is to allow: -* The desktop agent to confirm that it is connecting to an FDC3 Desktop Agent Bridge, rather than another service exposed via a websocket. -* The bridge to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents. -* The desktop agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. +* The Desktop Agent to confirm that it is connecting to Deskop Agent Bridge, rather than another service exposed via a websocket. +* The DAB to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents. +* The Desktop Agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. -The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. Desktop agents MUST accept the name they are assigned by the bridge. +The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. Desktop Agents MUST accept the name they are assigned by the bridge. -#### Step 1. Handshake - -Exchange standardized hello messages that identify: +Exchange standardized handshake messages that identify: * That the server is a bridge, including: * implementation details for logging by DA. @@ -132,15 +133,18 @@ sequenceDiagram participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C - DA ->>+ DAB: connect - DAB ->>+ DA: hello - DA ->>+ DAB: handshake - DAB ->>+ DA: connectedAgentsUpdate - DAB ->>+ DB: connectedAgentsUpdate - DAB ->>+ DC: connectedAgentsUpdate - + DA ->>+ DAB: connect (step 1) + DAB ->>+ DA: hello (step 2) + DA ->>+ DAB: handshake (steps 3-5) + DAB ->>+ DA: connectedAgentsUpdate (step 6) + DAB ->>+ DB: connectedAgentsUpdate (step 6) + DAB ->>+ DC: connectedAgentsUpdate (step 6) ``` +#### Step 1. Connect to Websocket +The Desktop Agent attempts to connect to the websocket at the first port in the defined port range. + +#### Step 2. Hello When a new connection is made to the DAB websocket, it sends a `hello` message, including its metadata. ```typescript @@ -161,9 +165,13 @@ When a new connection is made to the DAB websocket, it sends a `hello` message, A Desktop Agent can use the structure of this message to determine that it has connected to a Desktop Agent Bridge (i.e by checking `msg.type === "hello" && msg.payload.desktopAgentBridgeVersion`), whether it supports a compatible FDC3 version and whether it is expected to provide authentication credentials in the next step (`if(msg.payload.authRequired) { ... }`). -An optional JWT token can be included in the `hello` message to allow the connecting agent to authenticate the bridge. Verification of the JWM by the DA is option but recommended, meaning that the DA SHOULD verify the received JWT when one is included in the `hello` message. +An optional JWT token can be included in the `hello` message to allow the connecting agent to authenticate the bridge. Verification of the supplied JWT by the DA is optional but recommended, meaning that the DA SHOULD verify the received JWT when one is included in the `hello` message. + +If no hello message is received, the message doesn't match the defined format or validation of the optional JWT fails, the Desktop Agent should return to step 1 and attempt connection to the next port in the range. In the event that there are no ports reining in the range, the Desktop Agent SHOULD reset to the beginning of the range, MAY pause its attempts to connect and resume later. Note, if the Desktop Agent is configured to run at startup (of the user's machine) it is possible that the Desktop Agent Bridge may start later (or be restarted at some point). Hence, Desktop Agents SHOULD be capable of connecting to the bridge once they are already running (rather than purely at startup). + +#### Step 3. Handshake & Authentication -DA should respond to the `hello` message with a `handshake` request to the bridge, including an auth token (JWT) if required. +The DA must then respond to the `hello` message with a `handshake` request to the bridge, including an auth token (JWT) if required. ```typescript { @@ -189,7 +197,8 @@ DA should respond to the `hello` message with a `handshake` request to the bridg } ``` -#### Step 2. Authentication (optional?) +Note that the `meta` element of of the handshake message contains both a `timestamp` field (for logging purposes) and a `requestGuid` field that should be populated with a Globally Unique Identifier (GUID), generated by the Destkop Agent. This `responseGuid` will be used to link the handshake message to a response from the DAB that assigns it a name. For more details on GUID generation see section XX. + If requested by the server, the JWT auth token payload should take the form: @@ -210,7 +219,7 @@ e.g. Note that the `sub` SHOULD be a UUID that does NOT need to match the name requested by the Desktop Agent. It will be used to identify the keypair that should be used to validate the JWT token. Further, multiple Desktop Agent's MAY share the same keys for authentication and hence the same `sub`, but they will be assigned different names for routing purposes by the DAB. If an agent disconnects from the bridge and later re-connects it MAY request and be assigned the same name it connected with before. -#### Step 3. Auth Confirmation and Name Assignment +#### Step 4. Auth Confirmation and Name Assignment The DAB will extract the authentication token `sub` from the JWT token's claims and then verify the token's signature against any public key it has been configured with. If the signature can't be verified, the bridge should respond with the below authentication failed message and the socket should be disconnected by the bridge. @@ -228,41 +237,38 @@ The DAB will extract the authentication token `sub` from the JWT token's claims } ``` -If authentication succeeds (or is not required), then the DAB should assign the Desktop Agent, and associated socket connection, the name requested in the `handshake` message, unless another agent is already connected with that name in which case it should generate a new name which MAY be derived from the requested name. - -#### Step 4. Synchronize the bridge's channel state - -Channels are the only stateful mechanism in the FDC3 that we have to consider. A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so, the states must be synchronized whenever a new agent connects. Hence, the Bridge must process the `channelState` provided by the new agent in the `handshake` request containing details of each known User Channel or App Channel and its state, compare it to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to connected agents to ensure that they are synchronized with it. +If authentication succeeds (or is not required), then the DAB should assign the Desktop Agent the name requested in the `handshake` message, unless another agent is already connected with that name in which case it should generate a new name which MAY be derived from the requested name. Note that the assigned name is not communicated to the connecting agent until step 5. -Channel state of agents's already connected to the bridge should take precedence over agents that are connecting. However, if all agent's disconnect from the bridge it should reset (clear) its internal state and adopt that of the first agent to connect. The state of any Private channels does not need to be included as these can only be retrieved via a raised intent. +#### Step 5. Synchronize the bridge's channel state -When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast of context as it may result in an older context, of a type not already found on the channel being merged into the channel (and subsequently broadcast to context listeners with a matching type). +Channels are the main stateful mechanism in the FDC3 that we have to consider. A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so, the states must be synchronized whenever a new agent connects. Hence, the Bridge MUST process the `channelState` provided by the new agent in the `handshake` request, which MUST contain details of each known User Channel or App Channel and its state. The bridge MUST compare the received channel names and states to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to all connected agents to ensure that they are synchronized with it. -The incoming `channelsState` should be merged with the `existingChannelsState` as follows: +Hence, if we assume that the state of each channel can be represented by an ordered array of context objects (most recent first - noting that only the first position, that of the most recent context broadcast, matters), the Desktop Agent Bridge MUST merge the incoming `channelsState` with the `existingChannelsState` as follows: ```typescript Object.keys(channelsState).forEach((channelId) => { if (!existingChannelsState[channelId]) { - //unknown channel, just aodopt its state + //unknown channels: just adopt its state existingChannelsState[channelId] = channelsState[channelId]; } else { - //known channel merge state, with existing state taking precedence + //known channels: merge state, with existing state taking precedence const currentState = existingChannelsState[channelId]; const incoming = channelsState[channelId]; incoming.forEach((context) => { + //only add previously unknown context types to the state if (!currentState.find(element => element.type === context.type)){ + //add to end of array to avoid setting most recent context type at the beginning currentState.push(context); } - // ignore any types that are already known and - // preserve most recent channel state by adding to the end of the array + // else ignore any types that are already known }); } }); ``` -For the scenario of multiple agents connecting to the Desktop Agent Bridge at the same time, this portion of the handshake should be handled by the DAB serially to ensure correct channel state synchronization. +When multiple agents attempt to connect to the Desktop Agent Bridge at the same time, steps 3-6 of the connection protocol MUST be handled by the DAB serially to ensure correct channel state synchronization. -#### Step 5. Connected agents update +#### Step 6. Connected agents update The updated `existingChannelsState` will then be shared with all connected agents along with updated details of all connected agents via a `connectedAgentsUpdate` message sent to all connected sockets. The newly connected agent will receive both its assigned name and channel state via this message. The `connectedAgentsUpdate` message will be linked to the handshake request by quoting the `meta.requestGuid` of the `handshake` message. @@ -296,32 +302,38 @@ The `connectedAgentsUpdate` message will take the form: } ``` -When applying the updated channel state, it should be noted that desktop agents will not have context listeners for previously unknown channels, and can simply record that channel's state for use when that channel is first used. For known channel names, the Desktop Agent must also compare its current state to that which it has just received and may need to broadcast context to existing connected listeners. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as the most recently transmitted Context should be received by un-typed listeners). Hence, updating listeners for a known channel should be performed as follows: +When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast as the context being merged would become the most recent context on the channel, when other contexts may have been broadcast subsequently. Rather, it should be adopted internally by the Desktop Agent merging it such that it would be received by applications that are adding a user channel context listener or calling `channel.getCurrentContext()`. + +It should be noted that Deskop Agents will not have context listeners for previously unknown channels, and SHOULD simply record that channel's state for use when that channel is first used. + +For known channel names, the Desktop Agent MUST also compare its current state to that which it has just received and may need to internally adopt context of types not previously seen on a channel. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as only the most recently transmitted Context should be received by un-typed listeners). Hence, the new context MUST only be passed to a context listener if it was registered specifically for that type and a context of that type did not previously exist on the channel. -1. The incoming channel state `channelState` for a particular channel should be processed from last (oldest) to first. -2. If there is no current context of that type, broadcast it to listeners of that specific type only. +In summary, updating listeners for a known channel should be performed as follows: + +1. The incoming channel state `channelState` for a particular channel should be processed from last to first (most recent context broadcast). +2. If there is no current context of that type, broadcast it to any listeners of that specific type only. 3. If there is a current context of that type, and it does not match the incoming object exactly, broadcast it to listeners of that specific type only. 4. If the most recent (first in the incoming array) type OR value of that type doesn't match the most recent context broadcast on the channel, broadcast it to un-typed listeners only. -After applying the `connectedAgentsUpdate` message, the newly connected desktop agent and other already connected agents are able to begin communicating through the bridge. - -Channel state synchronization messages from the DAB to Desktop Agents should be handled atomically to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. +This procedure is the same for both previously connected and connecting agents, however, the merging procedure used by the DAB in step 5 will result in apps managed by previously connected agents only rarely receiving context broadcasts (and only for types they have not yet seen on a channel). -#### Step 6. Disconnects +After applying the `connectedAgentsUpdate` message, the newly connected Deskop Agent and other already connected agents are able to begin communicating through the bridge. -Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 5 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. +The handling of these synchronization messages from the DAB to Desktop Agents should be handled atomically by Desktop Agents to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. I.e. the `connectedAgentsUpdate` message must be processed immediately on receipt and updates applied before any other messages are sent or responses processed. +Similarly, the DAB must process `handshake` messages and issue `connectedAgentsUpdate` messages to all participants (steps 3-6) atomically, allowing no overlap with the processing of other messages from connected agents. +#### Step 7. Disconnects -## Interactions between Desktop Agents +Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 6 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. -The use of Desktop Agent Bridging affects how a desktop agent must handle FDC3 API calls. Details on how this affects the FD3 API and how a desktop agent should interact with other agents over the bridge are provided below in this section. +## Handling FDC3 Interactions When Bridged -### Handling FDC3 calls When Bridged +The use of Desktop Agent Bridging affects how a Desktop Agent must handle FDC3 API calls. Details on how this affects the FD3 API, how a Desktop Agent should interact the bridge and specifics of the messaging protocol are provided in this section. +### Messaging protocol TBC - describe generic protocol for working with the bridge for both fire and forget and request/response type calls. - ### Identifying Desktop Agents Identity and Message Sources In order to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. @@ -347,7 +359,7 @@ interface AppIdentifier { **/ readonly desktopAgent?: string; } -``` +``` ### Identifying Individual Messages @@ -356,13 +368,15 @@ There are a variety of message types we'll need to send between bridged DAs, sev * GUIDs required to uniquely identify messages * To be referenced in replies -This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response the does not reference a request GUID should be considered invalid. +This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response that does not reference a request GUID will be considered invalid. -### Forwarding of Messages to Other Agents +#### Global Unique Identifier -//TODO: rewrite based on DA deciding to send to bridge, bridge forwards on -//if target specified, forward to specific agent only +A GUID (globally unique identifier), also known as a Universally Unique IDentifier (UUID), is a generated 128-bit text string that is intended to be 'unique across space and time', as defined in [IETF RFC 4122](https://www.ietf.org/rfc/rfc4122.txt). +There are several types of GUIDs, which vary how they are generated. As Desktop Agents will typically be running on the same machine, system clock and hardware details may not provide sufficient uniqueness in GUIDs generated (including during the connect step, where Desktop Agent name collisions may exist). Hence, it is recommended that both Desktop Agents and Desktop Agent Bridges SHOULD use a version 4 generation type (random). + +### Forwarding of Messages to Other Agents The DAB MUST be able to forward messages received from one DA on to others (excluding obviously the Desktop Agent where the request was originated). There are a few simple rules which determine whether a message needs to be forwarded: @@ -371,11 +385,9 @@ The DAB MUST be able to forward messages received from one DA on to others (excl * The bridge will send it on to other known DAs (except the source of the message) * If the message has a target Desktop Agent (e.g. response to findIntent) - * The bridge will forward the message to it. - -## Generic request and response formats +## Request and Response Formats //TODO explain basic message structure type/payload (original FDC3 call args/response), meta (routing info). @@ -391,7 +403,7 @@ For simplicity, in this spec the request and response GUID will be just `request payload: { //example fields for specific messages... wouldn't be specified in base type channel?: string, - intent?: string, +Deskop Agent intent?: string, context?: Context, //fields for other possible arguments }, @@ -653,7 +665,7 @@ which is sent back over the bridge as a response to the request message as: ] } }, - "meta": { +Deskop Agent "meta": { "requestGuid": "requestGuid", "responseGuid": "responseGuidAgentB", "timestamp": "2020-03-...", @@ -705,7 +717,7 @@ This response gets repeated by the bridge in augmented form as: } ``` -DA agent-C would produce response: +Deskop AgentDA agent-C would produce response: ```JSON { From c14f26400dee5669e6e2d4b823bae85eaa1ee829 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 26 Jul 2022 16:36:27 +0100 Subject: [PATCH 34/61] Updating proposal structure and adding messaging protocol --- docs/api-bridging/spec.md | 273 +++++++++++++++++++++++--------------- 1 file changed, 167 insertions(+), 106 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index d40af3c7f..b192bb920 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -8,28 +8,34 @@ The FDC3 Desktop Agent API addresses interoperability between apps running withi In any Deskop Agent bridging scenario, it is expected that each DA is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although DAs may be run on different machines operated by the same user. +## Recent changes + +* Connection protocol updated + * Added initial connection step to connection protocol diagram + * Renumbered connection protocol steps to better map to diagram + * Added optional auth token to `hello` message from bridge to DA (allows 2-way authentication) + * Added description of atomicity requirements for handling of handshake and connectedAgentsUpdate messages by both DAB and DAs + * Channel State Synchronization + * Added details necessary to avoid race conditions (e.g. if a DA is connecting and another is broadcasting context concurrently) +* Recommended port range for bridge - (4470 - 4480) +* Interaction/Messaging protocol + * Added WIP draft + * Added section on GUID generation (with reference to external standard) used in requestGuid and responseGuid fields in messages. + * Handling of slow responding DAs? + * Recommended timeout of 2500ms/3000ms + + ## Open questions / TODO list -* Handshake - * Confirm we want to add optional auth token to `hello` message coming from the bridge for DA to authenticate the bridge? (Two-way authentication) -* Channel State Synchronization - * Add details about potential race conditions or workflows broken by disconnection - * Handling race conditions - e.g. on startup apps might load and start broadcasting state immediately - concurrent with the Deskop Agent connecting to the bridge and receiving state - * Handle handshakes sequentially one at a time - * Atomic operations for broadcasts and channel synchronization messages - * Other suggestions? -* Confirm recommended port range for bridge - (4470 - 4480) -* Define generic protocol for interacting with the bridge when handling fdc3 API calls - * Handle of slow responding DAs? - * Recommended timeout of 2500ms/3000ms +* Add details of how to handle: + * workflows broken by disconnection * An agent that is repeatedly timing out should be disconnected? * Advise on whether other agents report to users on connect/disconnect events? -* Add section on GUID generation (with reference to external standard) used in requestGuid and response Guid fields in messages. * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction -* Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to references page +* Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to FDC3 references page -## Connection Overview +## Implementing a Desktop Agent Bridge ### Topology @@ -91,13 +97,13 @@ The DAB is the responsible entity for collating responses together from all DAs. The DAB MUST allow for timeout configuration. -The Bridge SHOULD also implement timeout for waiting on DA responses. Assuming the message exchange will be all intra-machine, a recommended timeout of 2500ms/3000ms at most, should be implemented. +The Bridge SHOULD also implement timeout for waiting on DA responses. Assuming the message exchange will be all intra-machine, a recommended timeout of 1500ms - 3000ms should be used. #### Channels It is assumed that Desktop Agents SHOULD adopt the recommended 8 channel set (and the respective display metadata). Desktop Agents MAY support channel customization through configuration. -The Desktop Agent Bridge MAY support channel mapping ability to deal with channel differences that can arise. +The Desktop Agent Bridge MAY support channel mapping ability, to deal with issues caused by differing channel sets. A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync, which requires an initial synchronization step as part of the connection protocol. @@ -107,15 +113,15 @@ As the bridge binds its websocket on the loopback address (127.0.0.1) it cannot However, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each Deskop Agent simply communicating with a bridge instance located on the same machine. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. Further, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. However, methods of verifying the identity of user are currently beyond the scope of this Standard. -### Connection Protocol +## Connection Protocol On connection to the bridge's websocket, a handshake must be completed that may include an authentication step before a name is assigned to the Deskop Agent for use in routing messages. The purpose of the handshake is to allow: * The Desktop Agent to confirm that it is connecting to Deskop Agent Bridge, rather than another service exposed via a websocket. -* The DAB to require that the desktop agent authenticate itself, allowing it to control access to the network of bridged desktop agents. +* The DAB to require that the Desktop Agent authenticate itself, allowing it to control access to the network of bridged Desktop Agents. * The Desktop Agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. -The bridge is ultimately responsible for assigning each desktop agent a name and for routing messages using those names. Desktop Agents MUST accept the name they are assigned by the bridge. +The bridge is ultimately responsible for assigning each Desktop Agent a name and for routing messages using those names. Desktop Agents MUST accept the name they are assigned by the bridge. Exchange standardized handshake messages that identify: @@ -141,10 +147,12 @@ sequenceDiagram DAB ->>+ DC: connectedAgentsUpdate (step 6) ``` -#### Step 1. Connect to Websocket +### Step 1. Connect to Websocket + The Desktop Agent attempts to connect to the websocket at the first port in the defined port range. -#### Step 2. Hello +### Step 2. Hello + When a new connection is made to the DAB websocket, it sends a `hello` message, including its metadata. ```typescript @@ -169,7 +177,7 @@ An optional JWT token can be included in the `hello` message to allow the connec If no hello message is received, the message doesn't match the defined format or validation of the optional JWT fails, the Desktop Agent should return to step 1 and attempt connection to the next port in the range. In the event that there are no ports reining in the range, the Desktop Agent SHOULD reset to the beginning of the range, MAY pause its attempts to connect and resume later. Note, if the Desktop Agent is configured to run at startup (of the user's machine) it is possible that the Desktop Agent Bridge may start later (or be restarted at some point). Hence, Desktop Agents SHOULD be capable of connecting to the bridge once they are already running (rather than purely at startup). -#### Step 3. Handshake & Authentication +### Step 3. Handshake & Authentication The DA must then respond to the `hello` message with a `handshake` request to the bridge, including an auth token (JWT) if required. @@ -219,7 +227,7 @@ e.g. Note that the `sub` SHOULD be a UUID that does NOT need to match the name requested by the Desktop Agent. It will be used to identify the keypair that should be used to validate the JWT token. Further, multiple Desktop Agent's MAY share the same keys for authentication and hence the same `sub`, but they will be assigned different names for routing purposes by the DAB. If an agent disconnects from the bridge and later re-connects it MAY request and be assigned the same name it connected with before. -#### Step 4. Auth Confirmation and Name Assignment +### Step 4. Auth Confirmation and Name Assignment The DAB will extract the authentication token `sub` from the JWT token's claims and then verify the token's signature against any public key it has been configured with. If the signature can't be verified, the bridge should respond with the below authentication failed message and the socket should be disconnected by the bridge. @@ -239,7 +247,7 @@ The DAB will extract the authentication token `sub` from the JWT token's claims If authentication succeeds (or is not required), then the DAB should assign the Desktop Agent the name requested in the `handshake` message, unless another agent is already connected with that name in which case it should generate a new name which MAY be derived from the requested name. Note that the assigned name is not communicated to the connecting agent until step 5. -#### Step 5. Synchronize the bridge's channel state +### Step 5. Synchronize the bridge's channel state Channels are the main stateful mechanism in the FDC3 that we have to consider. A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so, the states must be synchronized whenever a new agent connects. Hence, the Bridge MUST process the `channelState` provided by the new agent in the `handshake` request, which MUST contain details of each known User Channel or App Channel and its state. The bridge MUST compare the received channel names and states to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to all connected agents to ensure that they are synchronized with it. @@ -268,7 +276,7 @@ Object.keys(channelsState).forEach((channelId) => { When multiple agents attempt to connect to the Desktop Agent Bridge at the same time, steps 3-6 of the connection protocol MUST be handled by the DAB serially to ensure correct channel state synchronization. -#### Step 6. Connected agents update +### Step 6. Connected agents update The updated `existingChannelsState` will then be shared with all connected agents along with updated details of all connected agents via a `connectedAgentsUpdate` message sent to all connected sockets. The newly connected agent will receive both its assigned name and channel state via this message. The `connectedAgentsUpdate` message will be linked to the handshake request by quoting the `meta.requestGuid` of the `handshake` message. @@ -323,132 +331,185 @@ The handling of these synchronization messages from the DAB to Desktop Agents sh Similarly, the DAB must process `handshake` messages and issue `connectedAgentsUpdate` messages to all participants (steps 3-6) atomically, allowing no overlap with the processing of other messages from connected agents. -#### Step 7. Disconnects +### Step 7. Disconnects Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 6 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. -## Handling FDC3 Interactions When Bridged - -The use of Desktop Agent Bridging affects how a Desktop Agent must handle FDC3 API calls. Details on how this affects the FD3 API, how a Desktop Agent should interact the bridge and specifics of the messaging protocol are provided in this section. - -### Messaging protocol -TBC - describe generic protocol for working with the bridge for both fire and forget and request/response type calls. - -### Identifying Desktop Agents Identity and Message Sources - -In order to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge, although they might request a particular identity. This allows for multiple copies of the same underlying desktop agent implementation to be bridged and ensures that id clashes can be avoided. +## Messaging protocol -To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppIdentifier` objects embedded in them. +In order for Desktop Agents to communicate with the Desktop Agent Bridge and thereby other Desktop Agents, a messaging protocol is required. FDC3 supports both 'fire and forget' interactions (such as the broadcast of context messages) and interactions with specific responses (such as raising intents and returning a resolution and optional result), both of which must be handled by that messaging protocol and message formats it defines, as described in this section. -* Sender details to be added by the DAB to the embedded `AppIdentifier` objects. - * `AppIdentifier` needs a new `desktopAgent` field - * When a client connects to a DAB it should be assigned an identity of some sort, which can be used to augment messages with details of the agent - * The DAB should do the assignments and could generate ids or accept them via config. - * DAs don't need to know their own ids or even the ids of others, they just need to be able to pass around `AppIdentifier` objects that contain them. +### Message Format -#### AppIdentifier - -`AppIdentifier` needs to be expanded to contain a `desktopAgent` field. +All messages sent or received by the Desktop Agent Bridge will be encoded in JSON and will have the same basic structure (including those already defined in the connection protocol): ```typescript -interface AppIdentifier { - readonly appId: string; - readonly instanceId?: string; - /** A string filled in by the bridge on receipt of a message, that represents - * the Desktop Agent that the app is available on. - **/ - readonly desktopAgent?: string; +{ + /** Identifier used to declare what aspect of FDC3 that the message relates to. */ + type: string, + /** Request body, containing any the arguments to the FDC3 interactions. */ + payload: { ... }, + /** Metadata relating to the message, its sender and destination. */ + meta: { ... } } -``` - -### Identifying Individual Messages - -There are a variety of message types we'll need to send between bridged DAs, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive and `IntentResponse` when an app has been chosen and the intent and context delivered to it). Hence, messages also need a unique identity. - -* GUIDs required to uniquely identify messages - * To be referenced in replies - -This means that each request that gets generated, should contain a request GUID, and every response to a request should contain a response GUID AND reference the request GUID. A response that does not reference a request GUID will be considered invalid. - -#### Global Unique Identifier - -A GUID (globally unique identifier), also known as a Universally Unique IDentifier (UUID), is a generated 128-bit text string that is intended to be 'unique across space and time', as defined in [IETF RFC 4122](https://www.ietf.org/rfc/rfc4122.txt). - -There are several types of GUIDs, which vary how they are generated. As Desktop Agents will typically be running on the same machine, system clock and hardware details may not provide sufficient uniqueness in GUIDs generated (including during the connect step, where Desktop Agent name collisions may exist). Hence, it is recommended that both Desktop Agents and Desktop Agent Bridges SHOULD use a version 4 generation type (random). - -### Forwarding of Messages to Other Agents - -The DAB MUST be able to forward messages received from one DA on to others (excluding obviously the Desktop Agent where the request was originated). There are a few simple rules which determine whether a message needs to be forwarded: - -* the message does not have a target Desktop Agent (e.g. findIntent) - * If you are a DA, send it on to the bridge - * The bridge will send it on to other known DAs (except the source of the message) - -* If the message has a target Desktop Agent (e.g. response to findIntent) +``` +Messages can be divided into two categories: -## Request and Response Formats +* Requests: Messages that initiate a particular interaction +* Responses: Messages that later to a prior request -//TODO explain basic message structure type/payload (original FDC3 call args/response), meta (routing info). +Details specific to each are provided below: -For simplicity, in this spec the request and response GUID will be just `requestGUID` and `responseGUID`. A GUID/UUID 128-bit integer number used to uniquely identify resources should be used. +#### Request Messages -### Request +Request messages use the following format: ```typescript { - /** FDC3 function name message relates to, e.g. "findIntent" */ + /** Typically set to the FDC3 function name that the message relates to, e.g. "findIntent" */ type: string, - /** Request body, containing the arguments to the function called.*/ + /** Request body, typically containing the arguments to the function called.*/ payload: { - //example fields for specific messages... wouldn't be specified in base type + //example fields for specific messages channel?: string, -Deskop Agent intent?: string, + intent?: string, context?: Context, - //fields for other possible arguments + app?: AppIdentifier }, + /** Metadata used to uniquely identify the message and its sender. */ meta: { - /** Unique guid for this request */ + /** Unique GUID for this request */ requestGuid: string, /** Timestamp at which request was generated */ timestamp: date, - /** AppIdentifier source request received from */ - source?: AppIdentifier + /** AppIdentifier for the source application that the request was + * received from and will be augmented with the assigned name of the + * Desktop Agent by the Desktop Agent Bridge, rather than the sender. */ + source: AppIdentifier + /** Optional AppIdentifier for the destination that the request should be + * routed to, which MUST be set by the Desktop Agent for API calls that + * include a target (`app`) parameter. MUST include the name of the + * Desktop Agent hosting the target application. */ + destination?: AppIdentifier } } ``` -### Response +If the FDC3 API call underlying the request message includes a target (typically defined by an `app` argument, in the form of an AppIdentifier object) it is the responsibility of the Desktop Agent to copy that argument into the `meta.destination` field of the message and to ensure that it includes a `meta.destination.desktopAgent` value. If the target is provided in the FDC3 API call, but without a `meta.destination.desktopAgent` value, the Destkop Agent should assume that the call relates to a local application and does not need to send it to the bridge. + +Requests without a `meta.destination` field will be forwarded to all other agents for processing and the collation of responses by the bridge. -Responses will be differentiated by the presence of a `responseGuid` field and MUST reference the `requestGuid` that they are responding to. +#### Response Messages + +Response messages will be differentiated from requests by the presence of a `responseGuid` field and MUST reference the `requestGuid` that they are responding to. ```typescript { /** FDC3 function name the original request related to, e.g. "findIntent" */ type: string, - /** Response body */ + /** Response body, containing the actual response data. */ payload: { - //example fields for specific messages... wouldn't be specified in base type + //example fields for specific messages... intent?: string, appIntent?: AppIntent, - //fields for other possible response values + + //TODO + + }, meta: { - /** Value from request*/ + /** requestGuid from the original request being responded to*/ requestGuid: string, - /** Unique guid for this response */ + /** Unique GUID for this response */ responseGuid: string, /** Timestamp at which request was generated */ timestamp: Date, - /** AppIdentifier source request received from */ - source?: AppIdentifier, - /** AppIdentifier destination response sent from */ - destination?: AppIdentifier + /** AppIdentifier for the source that generated this response */ + source: AppIdentifier, + /** AppIdentifier for the destination that the response should be routed to */ + destination: AppIdentifier //TODO determin if this is actually needed as we can use the requestGuid... } } ``` -DAs should send these messages on to the bridge, which will add the `source.desktopAgent` metadata. Further, when processing responses, the bridge should also augment any `AppIdentifier` objects in responses with the the same id applied to `source.desktopAgent`. +Response messages MUST always include a `meta.destination` field which matches the `meta.source` information provided in the request. Response messages that do not include a `meta.destination` should be discarded. + +### Identifying Desktop Agents Identity and Message Sources + +Desktop Agents will prepare messages in the above format and transmit them to the bridge. However, to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge. This allows for multiple copies of the same underlying Desktop Agent implementation to be bridged and ensures that id clashes can be avoided. + +To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation SHOULD retain a record of `requestGuid` fields, until teh request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. + +Further, the Destkop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Destkop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of such augmentation is provided in the description of each message exchange. + +#### AppIdentifier + +To facilitate addressing of messages to particular Desktop Agents and apps that they host, `AppIdentifier` is expanded to contain a `desktopAgent` field. + +```typescript +interface AppIdentifier { + readonly appId: string; + readonly instanceId?: string; + /** A string filled in by the bridge on receipt of a message, that represents + * the Desktop Agent that the app is available on. + **/ + readonly desktopAgent?: string; +} +``` + +### Identifying Individual Messages + +There are a variety of message types need to be sent between bridged DAs, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive an `IntentResolution` when an app has been chosen, and may subsequently receive an `IntentResult` after the intent handler has run). Hence, messages also need a unique identity, which should be generated at the Desktop Agent that is the source of that message, in the form of a Globally Unique Identifier (GUID). Response messages will include the identity of the request message they are related to, allowing multiple message exchanges to be 'in-flight' at the same time. + +Hence, whenever a request message is generated by a Desktop Agent it should contain a unique `meta.requestGuid` value. Response messages should quote that same value in the `meta.requestGuid` field and generate a further unique identity for their response, which is included in the `meta.responseGuid` field. + +Desktop Agent Bridge implementations should consider request messages that omit `meta.requestGuid` and response messages that omit either `meta.requestGuid` or `meta.responseGuid` to be invalid and should discard them. + +#### Globally Unique Identifier + +A GUID (globally unique identifier), also known as a Universally Unique IDentifier (UUID), is a generated 128-bit text string that is intended to be 'unique across space and time', as defined in [IETF RFC 4122](https://www.ietf.org/rfc/rfc4122.txt). + +There are several types of GUIDs, which vary how they are generated. As Desktop Agents will typically be running on the same machine, system clock and hardware details may not provide sufficient uniqueness in GUIDs generated (including during the connect step, where Desktop Agent name collisions may exist). Hence, it is recommended that both Desktop Agents and Desktop Agent Bridges SHOULD use a version 4 generation type (random). + +### Forwarding of Messages and Collating Responses + +When handling request messages, it is the responsibility of the Desktop Agent Bridge to: + +* Receive request messages from connected Destkop Agents, +* Augment request messages with source information (as described above), +* Forward request messages onto either a specific Desktop Agent or all Desktop Agents as appropriate. + +For message exchanges that involve responses, it is the responsibility of the Desktop Agent Bridge to: + +* Receive and collate response messages according the requestGuid (allowing multiple message exchanges to be 'in-flight' at once), +* Apply a timeout to the receipt of response messages for each request, +* Produce a single collated response message that incorporates the output of each individual response received, +* Deliver the collated response message to the source of the request + + +There are a few simple rules which determine how a request message should be forwarded: + +* If the message is a request (`meta.requestGuid` is set, but `meta.responseGuid` is not) + * and the message does not include a `meta.destination` field + * forward it to all other Desktop Agents (not including the source) and await responses + * else if a `meta.destination` was included + * forward it to the specified destination agent and await the response +* else if the message is a response + * and the message response includes + + + +## Handling FDC3 Interactions When Bridged + +//TODO add details of AppIdentifier augmentation to each exchange + +The use of Desktop Agent Bridging affects how a Desktop Agent must handle FDC3 API calls. Details on how this affects the FD3 API, how a Desktop Agent should interact the bridge and specifics of the messaging protocol are provided in this section. + + + + + ## Individual message exchanges @@ -593,7 +654,7 @@ Sends an outward message to the DAB. } ``` -The DAB fills in the `source.desktopAgent` field and forwards the request to the other desktop agents (agent-B AND agent-C). +The DAB fills in the `source.desktopAgent` field and forwards the request to the other Desktop Agents (agent-B AND agent-C). ```JSON // DAB -> agent-B @@ -855,7 +916,7 @@ agent-A sends an outward `findIntent` message to the DAB: "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { - "name": "someOtherApp", //should this be the desktop agent or the app? + "name": "someOtherApp", //should this be the Desktop Agent or the app? "appId": "...", "version": "...", // ... other metadata fields @@ -907,7 +968,7 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise agent-B From b308341d99b0a4195f1b677cd5468ab9642a45ea Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 27 Jul 2022 10:19:39 +0100 Subject: [PATCH 35/61] adding TODO message --- docs/api-bridging/spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index b192bb920..5d2e96206 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -498,7 +498,7 @@ There are a few simple rules which determine how a request message should be for * else if the message is a response * and the message response includes - +//TODO complete this ## Handling FDC3 Interactions When Bridged From a202302a218a6c6e6bca4017430a09afff3af3dc Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Wed, 27 Jul 2022 11:28:13 +0100 Subject: [PATCH 36/61] clean up --- docs/api-bridging/spec.md | 82 ++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 5d2e96206..0850a4036 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -4,15 +4,16 @@ sidebar_label: Overview title: API Bridging Overview (next) --- -The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent (DA), facilitating cross-application workflows. Desktop Agent API Bridging addresses the interconnection of Deskop Agents (DAs) such that apps running under different Deskop Agents can also interoperate, allowing workflows to span multiple Deskop Agents. +The FDC3 Desktop Agent API addresses interoperability between apps running within the context of a single Desktop Agent (DA), facilitating cross-application workflows. Desktop Agent API Bridging addresses the interconnection of Desktop Agents (DAs) such that apps running under different Desktop Agents can also interoperate, allowing workflows to span multiple Desktop Agents. -In any Deskop Agent bridging scenario, it is expected that each DA is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although DAs may be run on different machines operated by the same user. +In any Desktop Agent bridging scenario, it is expected that each DA is being operated by the same user (as the scope of FDC3 contemplates cross-application workflows for a single user, rather than cross-user workflows), although DAs may be run on different machines operated by the same user. -## Recent changes +## Recent Changes * Connection protocol updated * Added initial connection step to connection protocol diagram * Renumbered connection protocol steps to better map to diagram + * Improvements to connection protocol steps titles * Added optional auth token to `hello` message from bridge to DA (allows 2-way authentication) * Added description of atomicity requirements for handling of handshake and connectedAgentsUpdate messages by both DAB and DAs * Channel State Synchronization @@ -21,25 +22,24 @@ In any Deskop Agent bridging scenario, it is expected that each DA is being oper * Interaction/Messaging protocol * Added WIP draft * Added section on GUID generation (with reference to external standard) used in requestGuid and responseGuid fields in messages. - * Handling of slow responding DAs? - * Recommended timeout of 2500ms/3000ms - + * Added missing field comments for message formats +* Handling of slow responding DAs? + * Recommended timeout of 1500ms/3000ms ## Open questions / TODO list -* Add details of how to handle: +* Add details of how to handle: * workflows broken by disconnection * An agent that is repeatedly timing out should be disconnected? * Advise on whether other agents report to users on connect/disconnect events? * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction * Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to FDC3 references page - ## Implementing a Desktop Agent Bridge ### Topology -In order to implement Desktop Agent Bridging some means for Deskop Agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. +In order to implement Desktop Agent Bridging some means for Desktop Agents to connect to and communicate with each other is needed. This Standard assumes that Desktop Agent Bridging is implemented via a standalone 'bridge' which each agent connects to and will use to route messages to or from other agents. This topology is similar to a star topology in networking, where the Desktop Agent Bridge (a 'bridge') will be the central node acting as a router. ```mermaid flowchart TD; @@ -55,17 +55,17 @@ flowchart TD; E --> |loopback - 127.0.0.1| E ``` -Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by Deskop Agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. +Other possible topologies include peer-to-peer or client/server networks, however, these introduce significant additional complexity into multiple aspects of the bridging protocol that must be implemented by Desktop Agents, (including discovery, authentication and message routing), where a star topology/standalone bridge enables a relatively simple set of protocols, with the most difficult parts being implemented in the bridge itself. -Whilst the standalone bridge represents a single point of failure for the interconnection of Deskop Agents, it will also be significantly simpler than a full Deskop Agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then Deskop Agents will no longer be bridged and should act as single agents. +Whilst the standalone bridge represents a single point of failure for the interconnection of Desktop Agents, it will also be significantly simpler than a full Desktop Agent implementation. Further, failures may be mitigated by setting the bridge up as a system service, such that it is started when the user's computer is started and may be restarted automatically if it fails. In the event of a bridge failure or manual shutdown, then Desktop Agents will no longer be bridged and should act as single agents. -In Financial services it is not unusual for a single user to be working with applications on more than one desktop. As Deskop Agents do not span desktops bridging Deskop Agents across multiple machines is an additional use case for Deskop Agent bridging. However, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. +In Financial services it is not unusual for a single user to be working with applications on more than one desktop. As Desktop Agents do not span desktops bridging Desktop Agents across multiple machines is an additional use case for Desktop Agent bridging. However, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. ### Technology & Service Discovery -Connections between Deskop Agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected Deskop Agent as a client. +Connections between Desktop Agents and the Desktop Agent Bridge will be made via websocket connections, with the bridge acting as the websocket server and each connected Desktop Agent as a client. -The bridge MUST run on the same machine as the Deskop Agents, and the websocket MUST be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. +The bridge MUST run on the same machine as the Desktop Agents, and the websocket MUST be bound to the loopback adapter IP address (127.0.0.1), ensuring that the websocket is not exposed to wider networks. Bridge implementations SHOULD default to binding the websocket server to a port in the recommended port range 4475 - 4575, enabling simple discovery of a running bridge via attempting socket connections to ports in that range and attempting a handshake (as defined later in this proposal) that will identify the websocket as belong to a Desktop Agent Bridge. A port range is used, in preference to a single nominated port, in order to enable the automatic resolution of port clashes with other services. @@ -73,13 +73,13 @@ Both DAs and bridge implementations MUST support at least connection to the reco As part of the Desktop Agent Bridging protocol, a bridge will implement "server" behavior by: -* Accepting connections from client Deskop Agents, receiving and authenticating credentials and assigning a name (for routing purposes) -* Receiving requests from client Deskop Agents. -* Routing requests to client Deskop Agents. -* Receiving responses (and collating?) from client Deskop Agents. -* Routing responses to client Deskop Agents. +* Accepting connections from client Desktop Agents, receiving and authenticating credentials and assigning a name (for routing purposes) +* Receiving requests from client Desktop Agents. +* Routing requests to client Desktop Agents. +* Receiving responses (and collating?) from client Desktop Agents. +* Routing responses to client Desktop Agents. -A Deskop Agent will implement "client" behavior by: +A Desktop Agent will implement "client" behavior by: * Connecting to the bridge, providing authentication credentials and receiving an assigned named (for purposes) * Forwarding requests to the bridge. @@ -87,9 +87,9 @@ A Deskop Agent will implement "client" behavior by: * Receiving requests from the bridge. * Sending responses to the bridge. -Hence, message paths and propagation are simple. All messages to other Deskop Agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. +Hence, message paths and propagation are simple. All messages to other Desktop Agents are passed to the bridge for routing and all messages (both requests and responses) are received back from it, i.e. the bridge is responsible for all message routing. -#### Collating responses +#### Collating Responses Whilst some FDC3 requests are fire and forget (e.g. broadcast) the main requests such as `findIntent` or `raiseIntent` expect a response. In a bridging scenario, the response can come from multiple Desktop Agents and therefore need to be aggregated and augmented before they are sent back to the requesting DA. @@ -111,13 +111,13 @@ A key responsibility of the DAB is ensuring that the channel state of the connec As the bridge binds its websocket on the loopback address (127.0.0.1) it cannot be connected to from another device. Hence, an instance of the standalone bridge may be run on each device and those instances exchange messages in order to implement the bridge cross-device. -However, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each Deskop Agent simply communicating with a bridge instance located on the same machine. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. Further, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. However, methods of verifying the identity of user are currently beyond the scope of this Standard. +However, cross-machine routing is an internal concern of the Desktop Agent Bridge, with each Desktop Agent simply communicating with a bridge instance located on the same machine. The connection protocol between bridges themselves is implementation specific and beyond the scope of this standard. Further, as FDC3 only contemplates interoperability between apps for a single user, it is expected that in multi-machine use cases each machine is being operated by the same user. However, methods of verifying the identity of user are currently beyond the scope of this Standard. ## Connection Protocol -On connection to the bridge's websocket, a handshake must be completed that may include an authentication step before a name is assigned to the Deskop Agent for use in routing messages. The purpose of the handshake is to allow: +On connection to the bridge's websocket, a handshake must be completed that may include an authentication step before a name is assigned to the Desktop Agent for use in routing messages. The purpose of the handshake is to allow: -* The Desktop Agent to confirm that it is connecting to Deskop Agent Bridge, rather than another service exposed via a websocket. +* The Desktop Agent to confirm that it is connecting to Desktop Agent Bridge, rather than another service exposed via a websocket. * The DAB to require that the Desktop Agent authenticate itself, allowing it to control access to the network of bridged Desktop Agents. * The Desktop Agent to request a particular name by which it will be addressed by other agents and for the bridge to assign the requested name, after confirming that no other agent is connected with that name, or a derivative of that name if it is already in use. @@ -205,7 +205,7 @@ The DA must then respond to the `hello` message with a `handshake` request to th } ``` -Note that the `meta` element of of the handshake message contains both a `timestamp` field (for logging purposes) and a `requestGuid` field that should be populated with a Globally Unique Identifier (GUID), generated by the Destkop Agent. This `responseGuid` will be used to link the handshake message to a response from the DAB that assigns it a name. For more details on GUID generation see section XX. +Note that the `meta` element of of the handshake message contains both a `timestamp` field (for logging purposes) and a `requestGuid` field that should be populated with a Globally Unique Identifier (GUID), generated by the Desktop Agent. This `responseGuid` will be used to link the handshake message to a response from the DAB that assigns it a name. For more details on GUID generation see section XX. If requested by the server, the JWT auth token payload should take the form: @@ -247,7 +247,7 @@ The DAB will extract the authentication token `sub` from the JWT token's claims If authentication succeeds (or is not required), then the DAB should assign the Desktop Agent the name requested in the `handshake` message, unless another agent is already connected with that name in which case it should generate a new name which MAY be derived from the requested name. Note that the assigned name is not communicated to the connecting agent until step 5. -### Step 5. Synchronize the bridge's channel state +### Step 5. Synchronize the Bridge's Channel State Channels are the main stateful mechanism in the FDC3 that we have to consider. A key responsibility of the DAB is ensuring that the channel state of the connected agents is kept in-sync. To do so, the states must be synchronized whenever a new agent connects. Hence, the Bridge MUST process the `channelState` provided by the new agent in the `handshake` request, which MUST contain details of each known User Channel or App Channel and its state. The bridge MUST compare the received channel names and states to its own representation of the current state of channels in connected agents, merge that state with that of the new agent and communicate the updated state to all connected agents to ensure that they are synchronized with it. @@ -276,7 +276,7 @@ Object.keys(channelsState).forEach((channelId) => { When multiple agents attempt to connect to the Desktop Agent Bridge at the same time, steps 3-6 of the connection protocol MUST be handled by the DAB serially to ensure correct channel state synchronization. -### Step 6. Connected agents update +### Step 6. Connected Agents Update The updated `existingChannelsState` will then be shared with all connected agents along with updated details of all connected agents via a `connectedAgentsUpdate` message sent to all connected sockets. The newly connected agent will receive both its assigned name and channel state via this message. The `connectedAgentsUpdate` message will be linked to the handshake request by quoting the `meta.requestGuid` of the `handshake` message. @@ -312,7 +312,7 @@ The `connectedAgentsUpdate` message will take the form: When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast as the context being merged would become the most recent context on the channel, when other contexts may have been broadcast subsequently. Rather, it should be adopted internally by the Desktop Agent merging it such that it would be received by applications that are adding a user channel context listener or calling `channel.getCurrentContext()`. -It should be noted that Deskop Agents will not have context listeners for previously unknown channels, and SHOULD simply record that channel's state for use when that channel is first used. +It should be noted that Desktop Agents will not have context listeners for previously unknown channels, and SHOULD simply record that channel's state for use when that channel is first used. For known channel names, the Desktop Agent MUST also compare its current state to that which it has just received and may need to internally adopt context of types not previously seen on a channel. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as only the most recently transmitted Context should be received by un-typed listeners). Hence, the new context MUST only be passed to a context listener if it was registered specifically for that type and a context of that type did not previously exist on the channel. @@ -325,7 +325,7 @@ In summary, updating listeners for a known channel should be performed as follow This procedure is the same for both previously connected and connecting agents, however, the merging procedure used by the DAB in step 5 will result in apps managed by previously connected agents only rarely receiving context broadcasts (and only for types they have not yet seen on a channel). -After applying the `connectedAgentsUpdate` message, the newly connected Deskop Agent and other already connected agents are able to begin communicating through the bridge. +After applying the `connectedAgentsUpdate` message, the newly connected Desktop Agent and other already connected agents are able to begin communicating through the bridge. The handling of these synchronization messages from the DAB to Desktop Agents should be handled atomically by Desktop Agents to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. I.e. the `connectedAgentsUpdate` message must be processed immediately on receipt and updates applied before any other messages are sent or responses processed. @@ -335,7 +335,7 @@ Similarly, the DAB must process `handshake` messages and issue `connectedAgentsU Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 6 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. -## Messaging protocol +## Messaging Protocol In order for Desktop Agents to communicate with the Desktop Agent Bridge and thereby other Desktop Agents, a messaging protocol is required. FDC3 supports both 'fire and forget' interactions (such as the broadcast of context messages) and interactions with specific responses (such as raising intents and returning a resolution and optional result), both of which must be handled by that messaging protocol and message formats it defines, as described in this section. @@ -396,7 +396,7 @@ Request messages use the following format: } ``` -If the FDC3 API call underlying the request message includes a target (typically defined by an `app` argument, in the form of an AppIdentifier object) it is the responsibility of the Desktop Agent to copy that argument into the `meta.destination` field of the message and to ensure that it includes a `meta.destination.desktopAgent` value. If the target is provided in the FDC3 API call, but without a `meta.destination.desktopAgent` value, the Destkop Agent should assume that the call relates to a local application and does not need to send it to the bridge. +If the FDC3 API call underlying the request message includes a target (typically defined by an `app` argument, in the form of an AppIdentifier object) it is the responsibility of the Desktop Agent to copy that argument into the `meta.destination` field of the message and to ensure that it includes a `meta.destination.desktopAgent` value. If the target is provided in the FDC3 API call, but without a `meta.destination.desktopAgent` value, the Desktop Agent should assume that the call relates to a local application and does not need to send it to the bridge. Requests without a `meta.destination` field will be forwarded to all other agents for processing and the collation of responses by the bridge. @@ -428,7 +428,7 @@ Response messages will be differentiated from requests by the presence of a `res /** AppIdentifier for the source that generated this response */ source: AppIdentifier, /** AppIdentifier for the destination that the response should be routed to */ - destination: AppIdentifier //TODO determin if this is actually needed as we can use the requestGuid... + destination: AppIdentifier //TODO determine if this is actually needed as we can use the requestGuid... } } ``` @@ -441,7 +441,7 @@ Desktop Agents will prepare messages in the above format and transmit them to th To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation SHOULD retain a record of `requestGuid` fields, until teh request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. -Further, the Destkop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Destkop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of such augmentation is provided in the description of each message exchange. +Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of such augmentation is provided in the description of each message exchange. #### AppIdentifier @@ -476,7 +476,7 @@ There are several types of GUIDs, which vary how they are generated. As Desktop When handling request messages, it is the responsibility of the Desktop Agent Bridge to: -* Receive request messages from connected Destkop Agents, +* Receive request messages from connected Desktop Agents, * Augment request messages with source information (as described above), * Forward request messages onto either a specific Desktop Agent or all Desktop Agents as appropriate. @@ -487,7 +487,6 @@ For message exchanges that involve responses, it is the responsibility of the De * Produce a single collated response message that incorporates the output of each individual response received, * Deliver the collated response message to the source of the request - There are a few simple rules which determine how a request message should be forwarded: * If the message is a request (`meta.requestGuid` is set, but `meta.responseGuid` is not) @@ -496,7 +495,7 @@ There are a few simple rules which determine how a request message should be for * else if a `meta.destination` was included * forward it to the specified destination agent and await the response * else if the message is a response - * and the message response includes + * and the message response includes //TODO complete this @@ -506,11 +505,6 @@ There are a few simple rules which determine how a request message should be for The use of Desktop Agent Bridging affects how a Desktop Agent must handle FDC3 API calls. Details on how this affects the FD3 API, how a Desktop Agent should interact the bridge and specifics of the messaging protocol are provided in this section. - - - - - ## Individual message exchanges The sections below cover most scenarios for each of the Desktop Agent methods in order to explore how this protocol might work. @@ -726,7 +720,7 @@ which is sent back over the bridge as a response to the request message as: ] } }, -Deskop Agent "meta": { + "meta": { "requestGuid": "requestGuid", "responseGuid": "responseGuidAgentB", "timestamp": "2020-03-...", @@ -778,7 +772,7 @@ This response gets repeated by the bridge in augmented form as: } ``` -Deskop AgentDA agent-C would produce response: +DA agent-C would produce response: ```JSON { From 6d97396d5f5415f9c46fcdef51abffbe74a8f3e2 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Wed, 27 Jul 2022 11:41:32 +0100 Subject: [PATCH 37/61] Revert "Merge remote-tracking branch 'upstream/master' into 544-Desktop-Agent-Bridging-Proposal" This reverts commit 5d3020572ed7e2bbfd90620aef998405329c292d, reversing changes made to 471f90b3d57b678266de33f761a5fbcda02efece. --- CHANGELOG.md | 5 +- docs/api/ref/DesktopAgent.md | 107 +++--- docs/api/ref/Metadata.md | 13 +- docs/api/ref/Types.md | 2 +- docs/api/spec.md | 46 +-- docs/fdc3-intro.md | 2 +- docs/why-fdc3.md | 10 +- package.json | 4 +- src/api/ContextMetadata.ts | 6 +- src/api/DesktopAgent.ts | 142 +++----- src/api/IntentResolution.ts | 8 +- src/api/Methods.ts | 8 +- src/context/ContextTypes.ts | 326 ++++-------------- src/context/schemas.json | 7 +- test/Methods.test.ts | 9 - toolbox/fdc3-workbench/yarn.lock | 12 +- website/README.md | 34 +- website/data/community.json | 312 ----------------- website/data/implementations.json | 53 +++ website/data/pending-community.json | 12 - website/data/users.json | 50 +-- website/pages/en/get-involved.js | 2 +- .../en/{community.js => implementations.js} | 29 +- website/pages/en/index.js | 3 +- website/pages/fdc3-roadmap.html | 16 +- website/siteConfig.js | 2 +- website/static/css/custom.css | 12 - website/static/img/community/groups.png | Bin 4810 -> 0 bytes website/static/img/community/lf.png | Bin 44415 -> 0 bytes website/static/img/community/youtube.png | Bin 3983 -> 0 bytes website/static/img/users/FINOS.png | Bin 15414 -> 0 bytes website/static/img/users/GLUE42.png | Bin 7876 -> 11683 bytes .../img/users/Glue42 Enterprise Positive.svg | 1 - website/static/img/users/connectifi.png | Bin 4735 -> 0 bytes .../static/img/users/fdc3-desktop-agent.png | Bin 22198 -> 0 bytes website/static/img/users/fdc3-electron.png | Bin 13618 -> 0 bytes website/static/img/users/flextrade.jpg | Bin 106468 -> 0 bytes website/static/img/users/ipp-logo.png | Bin 14588 -> 0 bytes website/static/img/users/norman-and-sons.png | Bin 41067 -> 0 bytes website/static/img/users/pictet.png | Bin 194891 -> 0 bytes website/static/img/users/spglobal.png | Bin 47815 -> 0 bytes website/static/img/users/ubs.png | Bin 255513 -> 0 bytes website/static/js/implementationFilters.js | 2 +- yarn.lock | 25 +- 44 files changed, 293 insertions(+), 967 deletions(-) delete mode 100644 website/data/community.json create mode 100644 website/data/implementations.json delete mode 100644 website/data/pending-community.json rename website/pages/en/{community.js => implementations.js} (70%) delete mode 100644 website/static/img/community/groups.png delete mode 100644 website/static/img/community/lf.png delete mode 100644 website/static/img/community/youtube.png delete mode 100644 website/static/img/users/FINOS.png delete mode 100644 website/static/img/users/Glue42 Enterprise Positive.svg delete mode 100644 website/static/img/users/connectifi.png delete mode 100644 website/static/img/users/fdc3-desktop-agent.png delete mode 100644 website/static/img/users/fdc3-electron.png delete mode 100644 website/static/img/users/flextrade.jpg delete mode 100644 website/static/img/users/ipp-logo.png delete mode 100644 website/static/img/users/norman-and-sons.png delete mode 100644 website/static/img/users/pictet.png delete mode 100644 website/static/img/users/spglobal.png delete mode 100644 website/static/img/users/ubs.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 229ec2d01..7456fde07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Added a Trademarks page to website to acknowledge trademarks used within the Standard not owned by FINOS or the Linux Foundation ([#534](https://github.com/finos/FDC3/pull/534)) * Added details of FDC3's existing versioning and deprecation policies to the FDC3 compliance page ([#539](https://github.com/finos/FDC3/pull/539)) * Added a new experimental features policy, which exempts features designated as experimental from the versioning and deprecation policies, to the FDC3 compliance page ([#549](https://github.com/finos/FDC3/pull/549)) -* Added a recommended set of user channel definitions to the API docs and typescript sources ([#727](https://github.com/finos/FDC3/pull/727)) +* Added a recommended set of user channel definitions to the API docs and typescript sources ([#727](https://github.com/finos/FDC3/pull/726)) * Added the optional exposure of originating app metadata to messages received via `addContextListener` and `addIntentListener` via the new `ContextMetadata` type. ([#725](https://github.com/finos/FDC3/pull/725)) * Added the current app's `AppMetadata` to the `ImplementationMetadata` returned by `fdc3.getInfo()` allowing an app to retrieve its own metadata, according to the Desktop Agent ([#726](https://github.com/finos/FDC3/pull/726)) * Added a context type representing a range of time (`fdc3.timerange`). ([#706](https://github.com/finos/FDC3/pull/706)) @@ -45,7 +45,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * Added `categories` field and recommended categories list to AppD application records to enable category based browsing of AppDs ([#673](https://github.com/finos/FDC3/pull/673)) * Added an `interop` field to AppD application records, replacing the `intents` field, to more fully describe an app's use of FDC3 and enable search for apps that 'interoperate' with a selected app ([#697](https://github.com/finos/FDC3/pull/697)) * Added `AppIdentifier` type, which is a new parent of `AppMetadata` and clarifies required fields for API call parameters which now prefer `appId` and `instanceId` over `name` ([#722](https://github.com/finos/FDC3/pull/722)) -* Added a `getAppMetdata()` function to the desktop agent that can be used to retrieve the full `AppMetadata` for an `AppIdentifier` and reduced types such as `IntentResolution.source` and `ContextMetadata.source` from `AppMetadata` to `AppIdentifier` to clarify what fields a developer can rely on and that they should manually retrieve the full `AppMetadata` when they need it for display purposes. ([#751](https://github.com/finos/FDC3/pull/751)) ### Changed @@ -59,7 +58,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * The `fdc3.joinChannel()`, `fdc3.getCurrentChannel()` and `fdc3.leaveCurrentChannel()` functions have been made optional for FDC3 API compliance, but are recommended through the application of the SHOULD keyword. ([#512](https://github.com/finos/FDC3/pull/512)) * All DesktopAgent and Channel API functions are now async for consistency, changing the return type of the `broadcast`, `addIntentListener`, `addContextListener` and `getInfo` functions ([#516](https://github.com/finos/FDC3/pull/516)) * `IntentResolution` now requires the name of the intent raised to included, allowing it to be used to determine the intent raised via `fdc3.raiseIntentForContext()`. ([#507](https://github.com/finos/FDC3/pull/507)) -* The App Directory record schema (Application) has had the `manifestType` and `manifest` properties removed and replaced with the new `type` (required), `details` and `hostManifests` properties ([#437](https://github.com/finos/FDC3/pull/437)) * App Directory `images` field was replaced with `screenshots` to better align the application record with web application manifest and match its format to that used by `icons` ([#675](https://github.com/finos/FDC3/pull/675)) * API `AppMetadata` type was updated to replace the `images` field with a `screenshots` field (an array of `Image` objects) matching the spec of the App Directory's `screenshots` field entries ([#736](https://github.com/finos/FDC3/pull/736)) * App Directory endpoint for creating applications was removed as these will often be implementation dependent and should not be required for compliance ([#695](https://github.com/finos/FDC3/pull/695)) @@ -101,6 +99,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). * `addContextListener(contextType, handler)` now supports passing `null` as the context type ([#329](https://github.com/finos/FDC3/pull/329)) * All other API type changes and additions from the [FDC3 Standard 1.2](https://github.com/finos/FDC3/releases/tag/v1.2) release +* The Application schema by removing the `manifestType` and `manifest` properties, introducing new `type` (required), `details` and `hostManifests` properties ([#437](https://github.com/finos/FDC3/pull/437) ### Deprecated diff --git a/docs/api/ref/DesktopAgent.md b/docs/api/ref/DesktopAgent.md index 636273cd6..4f16ed178 100644 --- a/docs/api/ref/DesktopAgent.md +++ b/docs/api/ref/DesktopAgent.md @@ -15,9 +15,8 @@ It is expected that the `DesktopAgent` interface is made availabe via the [`wind ```ts interface DesktopAgent { // apps - open(app: AppIdentifier, context?: Context): Promise; - findInstances(app: AppIdentifier): Promise>; - getAppMetadata(app: AppIdentifier): Promise; + open(app: AppIdentifier, context?: Context): Promise; + findInstances(app: AppIdentifier): Promise>; // context broadcast(context: Context): Promise; @@ -47,7 +46,7 @@ interface DesktopAgent { addContextListener(handler: ContextHandler): Promise; getSystemChannels(): Promise>; joinChannel(channelId: string) : Promise; - open(name: String, context?: Context): Promise; + open(name: String, context?: Context): Promise; raiseIntent(intent: string, context: Context, name: String): Promise; raiseIntentForContext(context: Context, name: String): Promise; } @@ -61,7 +60,7 @@ interface DesktopAgent { addContextListener(contextType: string | null, handler: ContextHandler): Promise; ``` -Adds a listener for incoming context broadcasts from the Desktop Agent. If the consumer is only interested in a context of a particular type, they can specify that type. If the consumer is able to receive context of any type or will inspect types received, then they can pass `null` as the `contextType` parameter to receive all context types. +Adds a listener for incoming context broadcasts from the Desktop Agent. If the consumer is only interested in a context of a particular type, they can specify that type. If the consumer is able to receive context of any type or will inspect types received, then they can pass `null` as the `contextType` parameter to receive all context types. Context broadcasts are only received from apps that are joined to the same User Channel as the listening application, hence, if the application is not currently joined to a User Channel no broadcasts will be received. If this function is called after the app has already joined a channel and the channel already contains context that would be passed to the context listener, then it will be called immediately with that context. @@ -78,7 +77,7 @@ const contactListener = await fdc3.addContextListener('fdc3.contact', contact => // listener that logs metadata for the message a specific type const contactListener = await fdc3.addContextListener('fdc3.contact', (contact, metadata) => { - console.log(`Received context message\nContext: ${contact}\nOriginating app: ${metadata?.source}`); + console.log(`Received context message\nContext: ${contact}\nOriginating app: ${metadata?.sourceAppMetadata}`); //do something else with the context }); ``` @@ -114,7 +113,7 @@ const listener = fdc3.addIntentListener('StartChat', context => { //Handle a raised intent and log the originating app metadata const listener = fdc3.addIntentListener('StartChat', (contact, metadata) => { - console.log(`Received intent StartChat\nContext: ${contact}\nOriginating app: ${metadata?.source}`); + console.log(`Received intent StartChat\nContext: ${contact}\nOriginating app: ${metadata?.sourceAppMetadata}`); return; }); @@ -187,14 +186,14 @@ fdc3.broadcast(instrument); ### `findInstances` ```ts -findInstances(app: AppIdentifier): Promise>; +findInstances(app: AppIdentifier): Promise>; ``` Find all the available instances for a particular application. If there are no instances of the specified application the returned promise should resolve to an empty array. -If the request fails for another reason, the promise will return an `Error` with a string from the `ResolveError` enumeration. +If the resolution fails, the promise will return an `Error` with a string from the [`ResolveError`](Errors#resolveerror) enumeration. #### Example @@ -232,14 +231,14 @@ const appIntent = await fdc3.findIntent("StartChat"); // { // intent: { name: "StartChat", displayName: "Chat" }, // apps: [ -// { appId: "Skype" }, -// { appId: "Symphony" }, -// { appId: "Slack" } +// { name: "Skype" }, +// { name: "Symphony" }, +// { name: "Slack" } // ] // } // raise the intent against a particular app -await fdc3.raiseIntent(appIntent.intent.name, context, appIntent.apps[0]); +await fdc3.raiseIntent(appIntent.intent.name, context, appIntent.apps[0].name); //later, we want to raise 'StartChat' intent again const appIntent = await fdc3.findIntent("StartChat"); @@ -248,10 +247,10 @@ const appIntent = await fdc3.findIntent("StartChat"); // { // intent: { name: "StartChat", displayName: "Chat" }, // apps: [ -// { appId: "Skype" }, -// { appId: "Symphony" }, -// { appId: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, -// { appId: "Slack" } +// { name: "Skype" }, +// { name: "Symphony" }, +// { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, +// { name: "Slack" } // ] ``` @@ -270,14 +269,14 @@ const appIntent = await fdc3.findIntent("ViewContact", "fdc3.ContactList"); // returns only apps that return the specified result type: // { // intent: { name: "ViewContact", displayName: "View Contact Details" }, -// apps: { appId: "MyCRM", resultType: "fdc3.ContactList"}] +// apps: { name: "MyCRM", resultType: "fdc3.ContactList"}] // } const appIntent = await fdc3.findIntent("QuoteStream", instrument, "channel"); // returns only apps that return a channel which will receive the specified input and result types: // { // intent: { name: "QuoteStream", displayName: "Quotes stream" }, -// apps: { appId: "MyOMS", resultType: "channel"}] +// apps: { name: "MyOMS", resultType: "channel"}] // } ``` @@ -298,7 +297,7 @@ A promise resolving to all the intents, their metadata and metadata about the ap If the resolution fails, the promise will return an `Error` with a string from the [`ResolveError`](Errors#resolveerror) enumeration. -The optional `resultType` argument may be a type name, the string `"channel"` (which indicates that the app will return a channel) or a string indicating a channel that returns a specific type, e.g. `"channel"`. If intent resolution to an app returning a channel is requested without a specified context type, the desktop agent MUST include both apps that are registered as returning a channel and those registered as returning a channel with a specific type in the response. +The optional `resultType` argument may be a type name, the string `"channel"` (which indicates that the app will return a channel) or a string indicating a channel that returns a specific type, e.g. `"channel"`. If intent resolution to an app returning a channel is requested, the desktop agent MUST include both apps that are registered as returning a channel and those registered as returning a channel with a specific type in the response. #### Example @@ -311,20 +310,20 @@ const appIntents = await fdc3.findIntentsByContext(context); // [ // { // intent: { name: "StartCall", displayName: "Call" }, -// apps: [{ appId: "Skype" }] +// apps: [{ name: "Skype" }] // }, // { // intent: { name: "StartChat", displayName: "Chat" }, // apps: [ -// { appId: "Skype" }, -// { appId: "Symphony" }, -// { appId: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, -// { appId: "Slack" } +// { name: "Skype" }, +// { name: "Symphony" }, +// { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, +// { name: "Slack" } // ] // }, // { // intent: { name: "ViewContact", displayName: "View Contact" }, -// apps: [{ appId: "Symphony" }, { appId: "MyCRM", resultType: "fdc3.ContactList"}] +// apps: [{ name: "Symphony" }, { name: "MyCRM", resultType: "fdc3.ContactList"}] // } // ]; ``` @@ -336,7 +335,7 @@ const appIntentsForType = await fdc3.findIntentsByContext(context, "fdc3.Contact // returns for example: // [{ // intent: { name: "ViewContact", displayName: "View Contact" }, -// apps: [{ appId: "Symphony" }, { appId: "MyCRM", resultType: "fdc3.ContactList"}] +// apps: [{ name: "Symphony" }, { name: "MyCRM", resultType: "fdc3.ContactList"}] // }]; // select a particular intent to raise @@ -354,26 +353,6 @@ await fdc3.raiseIntent(startChat.intent.name, context, selectedApp); * [`findIntent()`](#findintent) * [`ResolveError`](Errors#resolveerror) -### `getAppMetadata` - -```ts -getAppMetadata(app: AppIdentifier): Promise; -``` - -Retrieves the [`AppMetadata`](Metadata#appmetadata) for an [`AppIdentifier`](Types#appidentifier), which provides additional metadata (such as icons, a title and description) from the App Directory record for the application, that may be used for display purposes. - -#### Examples - -```js -let appIdentifier = { appId: "MyAppId@my.appd.com" } -let appMetadata = await fdc3.getAppMetadata(appIdentifier); -``` - -#### See also - -* [`AppMetadata`](Metadata#appmetadata) -* [`AppIdentifier`](Types#appidentifier) - ### `getCurrentChannel` ```ts @@ -422,6 +401,7 @@ The `ImplementationMetadata` object returned also includes the metadata for the ```js let implementationMetadata = await fdc3.getInfo(); let {appId, instanceId} = implementationMetadata.appMetadata; + ``` #### See also @@ -435,9 +415,8 @@ let {appId, instanceId} = implementationMetadata.appMetadata; getOrCreateChannel(channelId: string): Promise; ``` -Returns a `Channel` object for the specified channel, creating it (as an _App_ channel) if it does not exist. - -If the Channel cannot be created or access was denied, the returned promise MUST be rejected with an error string from the `ChannelError` enumeration. +Returns a `Channel` object for the specified channel, creating it (as an _App_ channel) - if it does not exist. +`Error` with a string from the [`ChannelError`](Errors#channelerror) enumeration if the channel could not be created or access was denied. #### Example @@ -587,30 +566,33 @@ redChannel.addContextListener(null, channelListener); ### `open` ```ts -open(app: AppIdentifier, context?: Context): Promise; +open(app: AppIdentifier, context?: Context): Promise; ``` -Launches an app, specified via an [`AppIdentifier`](Metadata#appidentifier) object. +Launches an app with target information, which can be either be a string like a name, or an [`AppMetadata`](Metadata#appmetadata) object. -The `open` method differs in use from [`raiseIntent`](#raiseintent). Generally, it should be used when the target application is known but there is no specific intent. For example, if an application is querying an App Directory, `open` would be used to open an app returned in the search results. +The `open` method differs in use from [`raiseIntent`](#raiseintent). Generally, it should be used when the target application is known but there is no specific intent. For example, if an application is querying the App Directory, `open` would be used to open an app returned in the search results. **Note**, if the intent, context and target app name are all known, it is recommended to instead use [`raiseIntent`](#raiseintent) with the `target` argument. If a [`Context`](Types#context) object is passed in, this object will be provided to the opened application via a contextListener. The Context argument is functionally equivalent to opening the target app with no context and broadcasting the context directly to it. -Returns an [`AppIdentifier`](Metadata#appidentifier) object with the `instanceId` field set to identify the instance of the application opened by this call. +Returns an [`AppMetadata`](Metadata#appmetadata) object with the `instanceId` field set identifying the instance of the application opened by this call. If opening errors, it returns an `Error` with a string from the [`OpenError`](Errors#openerror) enumeration. #### Example ```js -// Open an app without context, using an AppIdentifier object to specify the target -let appIdentifier = { appId: 'myApp-v1.0.1' }; -let instanceIdentifier = await fdc3.open(appIdentifier); +// Open an app without context, using the app name +let instanceMetadata = await fdc3.open('myApp'); + +// Open an app without context, using an AppMetadata object to specify the target +let appMetadata = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; +let instanceMetadata = await fdc3.open(appMetadata); // Open an app with context -let instanceIdentifier = await fdc3.open(appIdentifier, context); +let instanceMetadata = await fdc3.open(appMetadata, context); ``` #### See also @@ -626,7 +608,7 @@ let instanceIdentifier = await fdc3.open(appIdentifier, context); raiseIntent(intent: string, context: Context, app?: AppIdentifier): Promise; ``` -Raises a specific intent for resolution against apps registered with the desktop agent. +Raises a specific intent for resolution against apps registered with the desktop agent. The desktop agent MUST resolve the correct app to target based on the provided intent name and context data. If multiple matching apps are found, a method for resolving the intent to a target app, such as presenting the user with a resolver UI allowing them to pick an app, SHOULD be provided. Alternatively, the specific app or app instance to target can also be provided. A list of valid target applications and instances can be retrieved via [`findIntent`](DesktopAgent#findintent). @@ -705,7 +687,7 @@ If a target app for the intent cannot be found with the criteria provided or the const intentResolution = await fdc3.raiseIntentForContext(context); // Resolve against all intents registered by a specific target app for the specified context -await fdc3.raiseIntentForContext(context, targetAppIdentifier); +await fdc3.raiseIntentForContext(context, targetAppMetadata); ``` #### See also @@ -727,9 +709,9 @@ addContextListener(handler: ContextHandler): Promise; Adds a listener for incoming context broadcasts from the Desktop Agent. Provided for backwards compatibility with versions FDC3 standard <2.0. #### See also - * [`addContextListener`](#addcontextlistener) + ### `getSystemChannels` (deprecated) ```ts @@ -738,7 +720,6 @@ getSystemChannels() : Promise>; Alias to the [`getUserChannels`](#getuserchannels) function provided for backwards compatibility with version 1.1 & 1.2 of the FDC3 standard. #### See also - * [`getUserChannels`](#getuserchannels) ### `joinChannel` (deprecated) @@ -755,7 +736,7 @@ Alias to the [`joinUserChannel`](#joinuserchannel) function provided for backwar ### `open` (deprecated) ```ts -open(name: String, context?: Context): Promise; +open(name: String, context?: Context): Promise; ``` Version of `open` that launches an app by name rather than `AppIdentifier`. Provided for backwards compatibility with versions of the FDC3 Standard <2.0. diff --git a/docs/api/ref/Metadata.md b/docs/api/ref/Metadata.md index 336dafbae..54895f4a9 100644 --- a/docs/api/ref/Metadata.md +++ b/docs/api/ref/Metadata.md @@ -32,6 +32,7 @@ For each intent, it reference the applications that support that intent. ```ts interface AppMetadata extends AppIdentifier { + /** /** * The 'friendly' app name. This field was used with the `open` and * `raiseIntent` calls in FDC3 <2.0, which now require an `AppIdentifier` @@ -101,10 +102,10 @@ Note that as `AppMetadata` instances are also `AppIdentifiers` they may be passe ```ts interface ContextMetadata { - /** Identifier for the app instance that sent the context and/or intent. - * @experimental + /** Metadata identifying the app that sent the context and/or intent. + * @experimental */ - readonly source: AppIdentifier; + readonly sourceAppMetadata: AppMetadata; } ``` @@ -307,11 +308,11 @@ The interface used to describe an intent within the platform. ```ts interface IntentResolution { - /** Identifier for the app instance that was selected (or started) to resolve + /** Metadata about the app instance that was selected (or started) to resolve * the intent. `source.instanceId` MUST be set, indicating the specific app * instance that received the intent. */ - readonly source: AppIdentifier; + readonly source: AppMetadata; /** The intent that was raised. May be used to determine which intent the user * chose in response to `fdc3.raiseIntentForContext()`. @@ -378,4 +379,4 @@ try { * [`DesktopAgent.raiseIntent`](DesktopAgent#raiseintent) * [`DesktopAgent.raiseIntentForContext`](DesktopAgent#raiseintentforcontext) -* [`AppIdentifier`](Types#appidentifier) +* [`TargetApp`](Types#targetapp) diff --git a/docs/api/ref/Types.md b/docs/api/ref/Types.md index 9d4c5868f..36d89975d 100644 --- a/docs/api/ref/Types.md +++ b/docs/api/ref/Types.md @@ -8,7 +8,7 @@ FDC3 API operations make use of several type declarations. Identifies an application, or instance of an application, and is used to target FDC3 API calls at specific applications. Will always include at least an `appId` property, which can be used with `fdc3.open`, `fdc3.raiseIntent` etc.. -If the `instanceId` field is set then the `AppIdentifier` object represents a specific instance of the application that may be addressed using that Id. +If the `instanceId` field is set then the `AppMetadata` object represents a specific instance of the application that may be addressed using that Id. ```ts interface AppIdentifier { diff --git a/docs/api/spec.md b/docs/api/spec.md index 7507c891c..c312dd589 100644 --- a/docs/api/spec.md +++ b/docs/api/spec.md @@ -107,31 +107,9 @@ An actual connection protocol between Desktop Agents is not currently available ## Functional Use Cases -### Open an Application - -Linking from one application to another is a critical basic workflow that the web revolutionized via the hyperlink. Supporting semantic addressing of applications across different technologies and platform domains greatly reduces friction in linking different applications into a single workflow. - -### Requesting Functionality From Another App - -Often, we want to link from one app to another to dynamically create a workflow. Enabling this without requiring prior knowledge between apps is a key goal of FDC3 and is implemented via the raising of [intents](../intents/spec), which represent a desired action, to be performed with a [context](../context/spec) supplied as input. - -Intents provide a way for an app to request functionality from another app and defer the discovery and launching of the destination app to the Desktop Agent. There are multiple models for interop that intents can support. - -- **Chain**: In this case the workflow is completely handed off from one app to another (similar to linking). Currently, this is the primary focus in FDC3. -- **Client-Service**: A Client invokes a Service via the Intent, the Service performs some function, then passes the workflow back to the Client. Typically, there is a data payload type associated with this intent that is published as the standard contract for the intent. -- **Remote API**: An app wants to remote an entire API that it owns to another App. In this case, the API for the App cannot be standardized. However, the FDC3 API can address how an App connects to another App in order to get access to a proprietary API. - -### Send/broadcast Context - -On the financial desktop, applications often want to broadcast [context](../context/spec) to any number of applications. Context sharing needs to support different groupings of applications, which is supported via the concept of 'channels', over which context is broadcast and received by other applications listening to the channel. - -In some cases, an application may want to communicate with a single application or service and to prevent other applications from participating in the communication. For single transactions, this can instead be implemented via a raised intent, which will be delivered to a single application that can, optionally, respond with data. Alternatively, it may instead respond with a [`Channel`](ref/Channel) or [`PrivateChannel`](ref/PrivateChannel) over which a stream of responses or a dialog can be supported. - ### Retrieve Metadata about the Desktop Agent implementation -An application may wish to retrieve information about the version of the FDC3 Standard supported by a Desktop Agent implementation and the name of the implementation provider. - -Since version 1.2 of the FDC3 Standard it may do so via the [`fdc3.getInfo()`](ref/DesktopAgent#getinfo) function. The metadata returned can be used, for example, to vary the behavior of an application based on the version supported by the Desktop Agent, e.g.: +From version 1.2 of the FDC3 specification, Desktop Agent implementations MUST provide a `fdc3.getInfo()` function to allow apps to retrieve information about the version of the FDC3 specification supported by a Desktop Agent implementation and the name of the implementation provider. This metadata can be used to vary the behavior of an application based on the version supported by the Desktop Agent, e.g.: ```js import {compareVersionNumbers, versionIsAtLeast} from '@finos/fdc3'; @@ -143,7 +121,7 @@ if (fdc3.getInfo && versionIsAtLeast(await fdc3.getInfo(), '1.2')) { } ``` -The [`ImplementationMetadata`](ref/Metadata#implementationmetadata) object returned also includes the metadata for the calling application, according to the Desktop Agent. This allows the application to retrieve its own `appId`, `instanceId` and other details, e.g.: +The `ImplementationMetadata` object returned also includes the metadata for the calling application, according to the Desktop Agent. This allows the application to retrieve its own `appId`, `instanceId` and other details, e.g.: ```js let implementationMetadata = await fdc3.getInfo(); @@ -151,15 +129,25 @@ let {appId, instanceId} = implementationMetadata.appMetadata; ``` -### Reference apps or app instance(s) and retrieve their metadata +### Open an Application by Name -To construct workflows between applications, you need to be able to reference specific applications and instances of those applications. +Linking from one application to another is a critical basic workflow that the web revolutionized via the hyperlink. Supporting semantic addressing of applications across different technologies and platform domains greatly reduces friction in linking different applications into a single workflow. -From version 2.0 of the FDC3 Standard, Desktop Agent functions that reference or return information about other applications do so via an [`AppIdentifier`](ref/Types#appidentifier) type. [`AppIdentifier`](ref/Types#appidentifier) references specific applications via an `appId` from an [App Directory](../app-directory/overview) record and instances of that application via an `instanceId` assigned by the Desktop Agent. +### Requesting Functionality From Another App -Additional metadata for an application can be retrieved via the [`fdc3.getAppMetadata(appIdentifier)`](ref/DesktopAgent#getappmetadata) function, which returns an [`AppMetadata`](ref/Metadata#appmetadata) object. The additional metadata may include a title, description, icons, etc., which may be used for display purposes. +Often, we want to link from one app to another to dynamically create a workflow. Enabling this without requiring prior knowledge between apps is a key goal of FDC3 and is implemented via the raising of [intents](../intents/spec), which represent a desired action, to be performed with a [context](../context/spec) supplied as input. + +Intents provide a way for an app to request functionality from another app and defer the discovery and launching of the destination app to the Desktop Agent. There are multiple models for interop that intents can support. -Identifiers for instances of an application may be retrieved via the [`fdc3.findInstances(appIdentifier)`](ref/DesktopAgent#findinstances) function. +- **Chain**: In this case the workflow is completely handed off from one app to another (similar to linking). Currently, this is the primary focus in FDC3. +- **Client-Service**: A Client invokes a Service via the Intent, the Service performs some function, then passes the workflow back to the Client. Typically, there is a data payload type associated with this intent that is published as the standard contract for the intent. +- **Remote API**: An app wants to remote an entire API that it owns to another App. In this case, the API for the App cannot be standardized. However, the FDC3 API can address how an App connects to another App in order to get access to a proprietary API. + +### Send/broadcast Context + +On the financial desktop, applications often want to broadcast [context](../context/spec) to any number of applications. Context sharing needs to support different groupings of applications, which is supported via the concept of 'channels', over which context is broadcast and received by other applications listening to the channel. + +In some cases, an application may want to communicate with a single application or service and to prevent other applications from participating in the communication. For single transactions, this can instead be implemented via a raised intent, which will be delivered to a single application that can, optionally, respond with data. Alternatively, it may instead respond with a [`Channel`](ref/Channel) or [`PrivateChannel`](ref/PrivateChannel) over which a stream of responses or a dialog can be supported. ## Raising Intents diff --git a/docs/fdc3-intro.md b/docs/fdc3-intro.md index 6f0d2aa60..596af13e2 100644 --- a/docs/fdc3-intro.md +++ b/docs/fdc3-intro.md @@ -31,7 +31,7 @@ From its inception, the standards have been informed by real-world [business use ## Who is using FDC3? -The Financial Desktop Connectivity and Collaboration Consortium (FDC3) standards are created and used by [leading organizations across the financial industry](/users). For more detail on who's using FDC3, developer tools, training and examples, see the [community page](/community). +The Financial Desktop Connectivity and Collaboration Consortium (FDC3) standards are created and used by [leading organizations across the financial industry](/users). ## How is FDC3 governed? diff --git a/docs/why-fdc3.md b/docs/why-fdc3.md index 6a09eba24..cdb4f4cac 100644 --- a/docs/why-fdc3.md +++ b/docs/why-fdc3.md @@ -6,16 +6,12 @@ title: Why FDC3? ## Why look for FDC3-enabled applications? -You want your business to move fast and use best of breed applications. Application integration has traditionally been a time consuming and costly exercise, meaning that once a set of applications supporting a workflow was established, changing parts of the workflow without very good reason was a no-go. - -The main goal of FDC3 is to standardize how applications communicate, without having defined inter-application workflows prior to being deployed. Applications that are FDC3-enabled can take part in a workflow on the desktop without any coding or manual integration, allowing you to replace one application with another application serving the same functions to the desktop (in FDC3 terms - supporting the same Intents and Context). +You want your business to move fast and use best of breed applications. Application integration has traditionally been a time consuming and costly exercise, meaning that once a set of applications supporting a workflow was established, changing parts of the workflow without very good reason was a no-go. The main goal of FDC3 is to standardize how applications communicate, without having defined inter-application workflows prior to being deployed. Applications that are FDC3 enabled can take part in a workflow on the desktop without any coding or manual integration, allowing you to replace one application with another application serving the same functions to the desktop (in FDC3 terms - supporting the same Intents and Context) ## Why should I FDC3-enable my applications? -There is a trend towards breaking up monolithic desktop applications, replacing them with adaptable workflows which involve the collaboration of multiple best-of-breed applications. Still, much of the integration on the desktop is done by the actual end-user; copy/paste between applications, exporting/importing CSV files etc.. - -Every application that has manual user input is a candidate for being FDC3-enabled, being able to demonstrate that your application can effectively take part in a workflow (without manual dual-entry or other tedious operations) is a easy route to happier users. Allowing your application to reach out to others is another way of extending the power of your offering; your app might not offer charting, but can let the end-user chart in an FDC3-enabled companion application based on context passed from your own app. +There is a trend towards breaking up monolithic desktop applications, replacing them with adaptable workflows which involve the collaboration of multiple best-of-breed applications. Still much of the integration on the desktop is done by the actual end-user; copy/paste between applications, exporting/importing CSV files etc. Every application that has manual user input is a candidate for being FDC3-enabled, being able to demonstrate that your application can effectively take part in a workflow (without manual dual-entry or other tedious operations) is a easy route to happier users. Allowing your application to reach out to others is another way of extending the power of your offering; your app might not offer charting, but can let the end-user chart in an FDC3 enabled companion application based on context passed from your own app. ## Why should my development team look at adopting FDC3? -Deploying effective end-user workflows with as little development effort as possible, should be the goal for all internal/platform integration development teams. Implementing or developing on a platform that is FDC3-enabled, if done right, results in more bang for the buck. FDC3 is all about (re)usability and low-touch integration. With an [App directory](appd-intro) in place and a platform to develop on, each new enabled app broadens the value of the workflow offering. +Deploying effective end-user workflows with as little development effort as possible, should be the goal for all internal/platform integration development teams. Implementing or developing on a platform that is FDC3 enabled, if done right, results in more bang for the buck. FDC3 is all about (re)usability and low-touch integration, with an [App directory](appd-intro) in place and a platform to develop on, each new enabled app broadens the value of the workflow offering. diff --git a/package.json b/package.json index 32776a181..f71e61726 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,12 @@ "test": "tsdx test --verbose", "lint": "tsdx lint src/api test", "prepare": "tsdx build", - "typegen": "cd src/context && quicktype -s schema --src-urls schemas.json -o ContextTypes.ts && cd ../.. && tsdx lint src/context --fix" + "typegen": "cd src/context && quicktype -s schema --src-urls schemas.json -o ContextTypes.ts" }, "peerDependencies": {}, "husky": { "hooks": { - "pre-commit": "tsdx lint src test" + "pre-commit": "tsdx lint" } }, "prettier": { diff --git a/src/api/ContextMetadata.ts b/src/api/ContextMetadata.ts index ae73d9938..f02aa87f1 100644 --- a/src/api/ContextMetadata.ts +++ b/src/api/ContextMetadata.ts @@ -3,7 +3,7 @@ * Copyright FINOS FDC3 contributors - see NOTICE file */ -import { AppIdentifier } from './AppIdentifier'; +import { AppMetadata } from './AppMetadata'; /** * Metadata relating to a context or intent and context received through the @@ -12,9 +12,9 @@ import { AppIdentifier } from './AppIdentifier'; * @experimental Introduced in FDC3 2.0 and may be refined by further changes outside the normal FDC3 versioning policy. */ export interface ContextMetadata { - /** Identifier for the app instance that sent the context and/or intent. + /** Metadata relating to the app that sent the context and/or intent. * * @experimental */ - readonly source: AppIdentifier; + readonly sourceAppMetadata: AppMetadata; } diff --git a/src/api/DesktopAgent.ts b/src/api/DesktopAgent.ts index ef338e9fb..9d085ab37 100644 --- a/src/api/DesktopAgent.ts +++ b/src/api/DesktopAgent.ts @@ -11,8 +11,8 @@ import { Listener } from './Listener'; import { Context } from '../context/ContextTypes'; import { ImplementationMetadata } from './ImplementationMetadata'; import { PrivateChannel } from './PrivateChannel'; -import { AppIdentifier } from './AppIdentifier'; import { AppMetadata } from './AppMetadata'; +import { AppIdentifier } from './AppIdentifier'; /** * A Desktop Agent is a desktop component (or aggregate of components) that serves as a @@ -26,7 +26,7 @@ import { AppMetadata } from './AppMetadata'; export interface DesktopAgent { /** - * Launches an app, specified via an `AppIdentifier` object. + * Launches an app. * * The `open` method differs in use from `raiseIntent`. Generally, it should be used when the target application is known but there is no specific intent. For example, if an application is querying the App Directory, `open` would be used to open an app returned in the search results. * @@ -34,34 +34,33 @@ export interface DesktopAgent { * * If a Context object is passed in, this object will be provided to the opened application via a contextListener. The Context argument is functionally equivalent to opening the target app with no context and broadcasting the context directly to it. * - * Returns an `AppIdentifier` object with the `instanceId` field set identifying the instance of the application opened by this call. + * Returns an `AppMetadata` object with the `instanceId` field set identifying the instance of the application opened by this call. * * If opening errors, it returns an `Error` with a string from the `OpenError` enumeration. * * ```javascript * //Open an app without context, using an AppIdentifier object to specify the target by `appId`. * let appIdentifier = {appId: 'myApp-v1.0.1'}; - * let instanceIdentifier = await fdc3.open(appIdentifier); + * let instanceMetadata = await fdc3.open(appIdentifier); * * //Open an app with context - * let instanceIdentifier = await fdc3.open(appIdentifier, context); + * let instanceMetadata = await fdc3.open(appIdentifier, context); * ``` */ - open(app: AppIdentifier, context?: Context): Promise; + open(app: AppIdentifier, context?: Context): Promise; /** - * Find out more information about a particular intent by passing its name, and optionally its context and/or a desired result context type. + * Find out more information about a particular intent by passing its name, and optionally its context and/or a desired result type. * - * `findIntent` is effectively granting programmatic access to the Desktop Agent's resolver. - * It returns a promise resolving to the intent, its metadata and metadata about the apps and app instances that registered that intent. + * findIntent is effectively granting programmatic access to the Desktop Agent's resolver. + * A promise resolving to the intent, its metadata and metadata about the apps and app instances that registered it is returned. * This can be used to raise the intent against a specific app or app instance. * * If the resolution fails, the promise will return an `Error` with a string from the `ResolveError` enumeration. * - * Result types may be a type name, the string "channel" (which indicates that the app + * Output types may be a type name, the string "channel" (which indicates that the app * will return a channel) or a string indicating a channel that returns a specific type, * e.g. "channel". - * * If intent resolution to an app returning a channel is requested, the desktop agent * MUST include both apps that are registered as returning a channel and those registered * as returning a channel with a specific type in the response. @@ -74,14 +73,14 @@ export interface DesktopAgent { * // { * // intent: { name: "StartChat", displayName: "Chat" }, * // apps: [ - * // { appId: "Skype" }, - * // { appId: "Symphony" }, - * // { appId: "Slack" } + * // { name: "Skype" }, + * // { name: "Symphony" }, + * // { name: "Slack" } * // ] * // } * * // raise the intent against a particular app - * await fdc3.raiseIntent(appIntent.intent.name, context, appIntent.apps[0]); + * await fdc3.raiseIntent(appIntent.intent.name, context, appIntent.apps[0].name); * * //later, we want to raise 'StartChat' intent again * const appIntent = await fdc3.findIntent("StartChat"); @@ -90,10 +89,10 @@ export interface DesktopAgent { * // { * // intent: { name: "StartChat", displayName: "Chat" }, * // apps: [ - * // { appId: "Skype" }, - * // { appId: "Symphony" }, - * // { appId: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, - * // { appId: "Slack" } + * // { name: "Skype" }, + * // { name: "Symphony" }, + * // { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, + * // { name: "Slack" } * // ] * ``` * @@ -105,7 +104,7 @@ export interface DesktopAgent { * // returns only apps that support the type of the specified input context: * // { * // intent: { name: "StartChat", displayName: "Chat" }, - * // apps: [{ appId: "Symphony" }] + * // apps: [{ name: "Symphony" }] * // } * * const appIntent = await fdc3.findIntent("ViewContact", contact, "fdc3.ContactList"); @@ -113,7 +112,7 @@ export interface DesktopAgent { * // returns only apps that return the specified result Context type: * // { * // intent: { name: "ViewContact", displayName: "View Contact Details" }, - * // apps: { appId: "MyCRM", resultType: "fdc3.ContactList"}] + * // apps: { name: "MyCRM", resultType: "fdc3.ContactList"}] * // } * * const appIntent = await fdc3.findIntent("QuoteStream", instrument, "channel"); @@ -121,7 +120,7 @@ export interface DesktopAgent { * // returns only apps that return a channel which will receive the specified input and result types: * // { * // intent: { name: "QuoteStream", displayName: "Quotes stream" }, - * // apps: [{ appId: "MyOMS", resultType: "channel"}] + * // apps: [{ name: "MyOMS", resultType: "channel"}] * // } * ``` */ @@ -130,16 +129,15 @@ export interface DesktopAgent { /** * Find all the available intents for a particular context, and optionally a desired result context type. * - * `findIntentsByContext` is effectively granting programmatic access to the Desktop Agent's resolver. + * findIntents is effectively granting programmatic access to the Desktop Agent's resolver. * A promise resolving to all the intents, their metadata and metadata about the apps and app instance that registered it is returned, based on the context types the intents have registered. * * If the resolution fails, the promise will return an `Error` with a string from the `ResolveError` enumeration. * - * The optional `resultType` argument may be a type name, the string "channel" (which indicates that the app - * should return a channel) or a string indicating a channel that returns a specific type, - * e.g. "channel". If intent resolution to an app returning a channel is requested without - * a specified context type, the desktop agent MUST also include apps that are registered as returning a - * channel with a specific type in the response. + * Result types may be a type name, the string "channel" (which indicates that the app should return a + * channel) or a string indicating a channel that returns a specific type, e.g. "channel". + * If intent resolution to an app returning a channel is requested, the desktop agent MUST also include apps + * that are registered as returning a channel with a specific type in the response. * * ```javascript * // I have a context object, and I want to know what I can do with it, hence, I look for intents and apps to resolve them... @@ -154,10 +152,10 @@ export interface DesktopAgent { * // { * // intent: { name: "StartChat", displayName: "Chat" }, * // apps: [ - * // { appId: "Skype" }, - * // { appId: "Symphony" }, - * // { appId: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, - * // { appId: "Slack" } + * // { name: "Skype" }, + * // { name: "Symphony" }, + * // { name: "Symphony", instanceId: "93d2fe3e-a66c-41e1-b80b-246b87120859" }, + * // { name: "Slack" } * // ] * // } * // ]; @@ -167,7 +165,7 @@ export interface DesktopAgent { * // returns for example: * // [{ * // intent: { name: "ViewContact", displayName: "View Contacts" }, - * // apps: [{ appId: "MyCRM", resultType: "fdc3.ContactList"}] + * // apps: [{ name: "MyCRM", resultType: "fdc3.ContactList"}] * // }]; * * // select a particular intent to raise @@ -198,18 +196,13 @@ export interface DesktopAgent { * ``` * @param app */ - findInstances(app: AppIdentifier): Promise>; + findInstances(app: AppIdentifier): Promise>; /** * Publishes context to other apps on the desktop. Calling `broadcast` at the `DesktopAgent` scope will push the context to whatever _User Channel_ the app is joined to. If the app is not currently joined to a channel, calling `fdc3.broadcast` will have no effect. Apps can still directly broadcast and listen to context on any channel via the methods on the `Channel` class. * * DesktopAgent implementations should ensure that context messages broadcast to a channel by an application joined to it should not be delivered back to that same application. * - * If you are working with complex context types composed of other simpler types then you should broadcast - * each individual type (starting with the simpler types, followed by the complex type) that you want other - * apps to be able to respond to. Doing so allows applications to filter the context types they receive by - * adding listeners for specific context types. - * * ```javascript * const instrument = { * type: 'fdc3.instrument', @@ -285,7 +278,7 @@ export interface DesktopAgent { * await fdc3.raiseIntentForContext(context); * * // Resolve against all intents registered by a specific target app for the specified context - * await fdc3.raiseIntentForContext(context, targetAppIdentifier); + * await fdc3.raiseIntentForContext(context, targetAppMetadata); * ``` */ raiseIntentForContext(context: Context, app?: AppIdentifier): Promise; @@ -306,12 +299,6 @@ export interface DesktopAgent { * return; * }); * - * //Handle a raised intent and log the originating app metadata - * const listener = fdc3.addIntentListener('StartChat', (contact, metadata) => { - * console.log(`Received intent StartChat\nContext: ${contact}\nOriginating app: ${metadata?.source}`); - * return; - * }); - * * //Handle a raised intent and return Context data via a promise * fdc3.addIntentListener("CreateOrder", (context) => { * return new Promise((resolve) => { @@ -346,23 +333,16 @@ export interface DesktopAgent { /** * Adds a listener for incoming context broadcasts from the Desktop Agent via User channels. If the consumer is only interested in a context of a particular type, they can they can specify that type. If the consumer is able to receive context of any type or will inspect types received, then they can pass `null` as the `contextType` parameter to receive all context types. - * * Context broadcasts are only received from apps that are joined to the same User channel as the listening application, hence, if the application is not currently joined to a channel no broadcasts will be received. If this function is called after the app has already joined a channel and the channel already contains context that would be passed to the context listener, then it will be called immediately with that context. * + * * Optional metadata about the context message, including the app that originated the message, SHOULD be provided by the desktop agent implementation. * * ```javascript * // any context - * const listener = await fdc3.addContextListener(null, context => { ... }); - * + * const listener = fdc3.addContextListener(null, context => { ... }); * // listener for a specific type - * const contactListener = await fdc3.addContextListener('fdc3.contact', contact => { ... }); - * - * // listener that logs metadata for the message a specific type - * const contactListener = await fdc3.addContextListener('fdc3.contact', (contact, metadata) => { - * console.log(`Received context message\nContext: ${contact}\nOriginating app: ${metadata?.source}`); - * //do something else with the context - * }); + * const contactListener = fdc3.addContextListener('fdc3.contact', contact => { ... }); * ``` */ addContextListener(contextType: string | null, handler: ContextHandler): Promise; @@ -376,13 +356,9 @@ export interface DesktopAgent { * Optional function that joins the app to the specified User channel. In most cases, applications SHOULD be joined to channels via UX provided to the application by the desktop agent, rather than calling this function directly. * * If an app is joined to a channel, all `fdc3.broadcast` calls will go to the channel, and all listeners assigned via `fdc3.addContextListener` will listen on the channel. - * * If the channel already contains context that would be passed to context listeners assed via `fdc3.addContextListener` then those listeners will be called immediately with that context. - * * An app can only be joined to one channel at a time. - * * Rejects with an error if the channel is unavailable or the join request is denied. The error string will be drawn from the `ChannelError` enumeration. - * * ```javascript * // get all system channels * const channels = await fdc3.getUserChannels(); @@ -394,9 +370,9 @@ export interface DesktopAgent { joinUserChannel(channelId: string): Promise; /** - * Returns a `Channel` object for the specified channel, creating it (as an _App_ channel) if it does not exist. + * Returns a channel with the given identity. Either stands up a new channel or returns an existing channel. It is up to applications to manage how to share knowledge of these custom channels across windows and to manage channel ownership and lifecycle. * - * If the Channel cannot be created or access was denied, the returned promise MUST be rejected with an error string from the `ChannelError` enumeration. + * If the Channel cannot be created, the returned promise MUST be rejected with an error string from the `ChannelError` enumeration. * * ```javascript * try { @@ -475,46 +451,12 @@ export interface DesktopAgent { leaveCurrentChannel(): Promise; /** - * Retrieves information about the FDC3 Desktop Agent implementation, including the supported version - * of the FDC3 specification, the name of the provider of the implementation, its own version number - * and the metadata of the calling application according to the desktop agent. - * - * Returns an `ImplementationMetadata` object. This metadata object can be used to vary the behavior - * of an application based on the version supported by the Desktop Agent and for logging purposes. - * - * ```js - * import {compareVersionNumbers, versionIsAtLeast} from '@finos/fdc3'; - * - * if (fdc3.getInfo && versionIsAtLeast(await fdc3.getInfo(), "1.2")) { - * await fdc3.raiseIntentForContext(context); - * } else { - * await fdc3.raiseIntent("ViewChart", context); - * } - * ``` - * - * The `ImplementationMetadata` object returned also includes the metadata for the calling application, - * according to the Desktop Agent. This allows the application to retrieve its own `appId`, `instanceId` - * and other details, e.g.: - * - * ```js - * let implementationMetadata = await fdc3.getInfo(); - * let {appId, instanceId} = implementationMetadata.appMetadata; - * ``` + * Retrieves information about the FDC3 Desktop Agent implementation, such as + * the implemented version of the FDC3 specification and the name of the implementation + * provider. */ getInfo(): Promise; - /** - * Retrieves the `AppMetadata` for an `AppIdentifier`, which provides additional metadata (such as icons, - * a title and description) from the App Directory record for the application, that may be used for display - * purposes. - * - * ```js - * let appIdentifier = { appId: "MyAppId@my.appd.com" } - * let appMetadata = await fdc3.getAppMetadata(appIdentifier); - * ``` - */ - getAppMetadata(app: AppIdentifier): Promise; - //--------------------------------------------------------------------------------------------- //Deprecated function signatures //--------------------------------------------------------------------------------------------- @@ -543,7 +485,7 @@ export interface DesktopAgent { * let instanceMetadata = await fdc3.open('myApp'); * ``` */ - open(name: String, context?: Context): Promise; + open(name: String, context?: Context): Promise; /** * @deprecated version of `raiseIntent` that targets an app by by name rather than `AppIdentifier`. Provided for backwards compatibility with versions FDC3 standard <2.0. diff --git a/src/api/IntentResolution.ts b/src/api/IntentResolution.ts index 81fd90fb6..9571f1af8 100644 --- a/src/api/IntentResolution.ts +++ b/src/api/IntentResolution.ts @@ -4,11 +4,10 @@ */ import { IntentResult } from './Types'; -import { AppIdentifier } from './AppIdentifier'; +import { AppMetadata } from './AppMetadata'; /** * IntentResolution provides a standard format for data returned upon resolving an intent. - * * ```javascript * //resolve a "Chain" type intent * let resolution = await agent.raiseIntent("intentName", context); @@ -27,18 +26,17 @@ import { AppIdentifier } from './AppIdentifier'; * } catch(error) { * console.error(`${resolution.source} returned an error: ${error}`); * } - * * // Use metadata about the resolving app instance to target a further intent * await agent.raiseIntent("intentName", context, resolution.source); * ``` */ export interface IntentResolution { /** - * Identifier for the app instance that was selected (or started) to resolve the intent. + * Metadata about the app instance that was selected (or started) to resolve the intent. * `source.instanceId` MUST be set, indicating the specific app instance that * received the intent. */ - readonly source: AppIdentifier; + readonly source: AppMetadata; /** * The intent that was raised. May be used to determine which intent the user * chose in response to `fdc3.raiseIntentForContext()`. diff --git a/src/api/Methods.ts b/src/api/Methods.ts index a5d6ac52c..61222fb46 100644 --- a/src/api/Methods.ts +++ b/src/api/Methods.ts @@ -1,5 +1,6 @@ import { AppIdentifier, + AppMetadata, AppIntent, Channel, Context, @@ -8,7 +9,6 @@ import { IntentResolution, Listener, ImplementationMetadata, - AppMetadata, } from '..'; const DEFAULT_TIMEOUT = 5000; @@ -61,7 +61,7 @@ function isString(app: AppIdentifier | String): app is String { return typeof app === 'string'; } -export function open(app: AppIdentifier | String, context?: Context): Promise { +export function open(app: AppIdentifier | String, context?: Context): Promise { if (isString(app)) { return rejectIfNoGlobal(() => window.fdc3.open(app, context)); } else { @@ -161,10 +161,6 @@ export function getInfo(): Promise { return rejectIfNoGlobal(() => window.fdc3.getInfo()); } -export function getAppMetadata(app: AppIdentifier): Promise { - return rejectIfNoGlobal(() => window.fdc3.getAppMetadata(app)); -} - /** * Compare numeric semver version number strings (in the form `1.2.3`). * diff --git a/src/context/ContextTypes.ts b/src/context/ContextTypes.ts index 40c5c8126..892095e05 100644 --- a/src/context/ContextTypes.ts +++ b/src/context/ContextTypes.ts @@ -1,23 +1,17 @@ // To parse this data: // -// import { Convert, Context, Chart, ChatInitSettings, Contact, ContactList, Country, Currency, Email, Instrument, InstrumentList, Nothing, Organization, Portfolio, Position, TimeRange, Valuation } from "./file"; +// import { Convert, Context, Contact, ContactList, Instrument, InstrumentList, Country, Organization, Portfolio, Position, Nothing } from "./file"; // // const context = Convert.toContext(json); -// const chart = Convert.toChart(json); -// const chatInitSettings = Convert.toChatInitSettings(json); // const contact = Convert.toContact(json); // const contactList = Convert.toContactList(json); -// const country = Convert.toCountry(json); -// const currency = Convert.toCurrency(json); -// const email = Convert.toEmail(json); // const instrument = Convert.toInstrument(json); // const instrumentList = Convert.toInstrumentList(json); -// const nothing = Convert.toNothing(json); +// const country = Convert.toCountry(json); // const organization = Convert.toOrganization(json); // const portfolio = Convert.toPortfolio(json); // const position = Convert.toPosition(json); -// const timeRange = Convert.toTimeRange(json); -// const valuation = Convert.toValuation(json); +// const nothing = Convert.toNothing(json); // // These functions will throw an error if the JSON doesn't // match the expected interface, even if the JSON is valid. @@ -28,63 +22,6 @@ export interface Context { type: string; } -export interface Chart { - instruments: Instrument[]; - otherConfig?: { [key: string]: any }; - range?: TimeRange; - style?: Style; - type: string; - id?: { [key: string]: string }; - name?: string; -} - -export interface Instrument { - id: InstrumentID; - type: string; - name?: string; -} - -export interface InstrumentID { - BBG?: string; - CUSIP?: string; - FDS_ID?: string; - FIGI?: string; - ISIN?: string; - PERMID?: string; - RIC?: string; - SEDOL?: string; - ticker?: string; -} - -export interface TimeRange { - endTime?: Date; - startTime?: Date; - type: string; - id?: { [key: string]: string }; - name?: string; -} - -export enum Style { - Bar = 'bar', - Candle = 'candle', - Custom = 'custom', - Heatmap = 'heatmap', - Histogram = 'histogram', - Line = 'line', - Mountain = 'mountain', - Pie = 'pie', - Scatter = 'scatter', - StackedBar = 'stacked-bar', -} - -export interface ChatInitSettings { - chatName?: string; - initMessage?: string; - members?: ContactList; - options?: any; - type: any; -} - export interface ContactList { contacts: Contact[]; type: string; @@ -103,61 +40,48 @@ export interface ContactID { FDS_ID?: string; } -export interface Country { - id: CountryID; - type: string; - name?: string; -} - -export interface CountryID { - COUNTRY_ISOALPHA2?: string; - COUNTRY_ISOALPHA3?: string; - ISOALPHA2?: string; - ISOALPHA3?: string; -} - -export interface Currency { - id: CurrencyID; - name?: string; - type: string; -} - -export interface CurrencyID { - CURRENCY_ISOCODE?: string; -} - export interface Email { - recipients: RecipientsObject; + type: string; + recipients: object; subject?: string; textBody?: string; + richTextBody?: object; +} + +export interface InstrumentList { + instruments: Instrument[]; type: string; id?: { [key: string]: string }; name?: string; } -export interface RecipientsObject { - id?: RecipientsID; +export interface Instrument { + id: InstrumentID; type: string; name?: string; - contacts?: Contact[]; } -export interface RecipientsID { - email?: string; +export interface InstrumentID { + BBG?: string; + CUSIP?: string; FDS_ID?: string; + FIGI?: string; + ISIN?: string; + PERMID?: string; + RIC?: string; + SEDOL?: string; + ticker?: string; } -export interface InstrumentList { - instruments: Instrument[]; +export interface Country { + id: CountryID; type: string; - id?: { [key: string]: string }; name?: string; } -export interface Nothing { - type: string; - id?: { [key: string]: string }; - name?: string; +export interface CountryID { + ISOALPHA2?: string; + ISOALPHA3?: string; } export interface Organization { @@ -187,16 +111,8 @@ export interface Position { name?: string; } -export interface Valuation { - CURRENCY_ISCODE?: string; - expiryTime?: Date; - price?: number; +export interface Nothing { type: string; - valuationTime?: Date; - value: number; - CURRENCY_ISOCODE: any; - id?: { [key: string]: string }; - name?: string; } // Converts JSON strings to/from your types @@ -210,22 +126,6 @@ export class Convert { return JSON.stringify(uncast(value, r('Context')), null, 2); } - public static toChart(json: string): Chart { - return cast(JSON.parse(json), r('Chart')); - } - - public static chartToJson(value: Chart): string { - return JSON.stringify(uncast(value, r('Chart')), null, 2); - } - - public static toChatInitSettings(json: string): ChatInitSettings { - return cast(JSON.parse(json), r('ChatInitSettings')); - } - - public static chatInitSettingsToJson(value: ChatInitSettings): string { - return JSON.stringify(uncast(value, r('ChatInitSettings')), null, 2); - } - public static toContact(json: string): Contact { return cast(JSON.parse(json), r('Contact')); } @@ -242,22 +142,6 @@ export class Convert { return JSON.stringify(uncast(value, r('ContactList')), null, 2); } - public static toCountry(json: string): Country { - return cast(JSON.parse(json), r('Country')); - } - - public static countryToJson(value: Country): string { - return JSON.stringify(uncast(value, r('Country')), null, 2); - } - - public static toCurrency(json: string): Currency { - return cast(JSON.parse(json), r('Currency')); - } - - public static currencyToJson(value: Currency): string { - return JSON.stringify(uncast(value, r('Currency')), null, 2); - } - public static toEmail(json: string): Email { return cast(JSON.parse(json), r('Email')); } @@ -282,12 +166,12 @@ export class Convert { return JSON.stringify(uncast(value, r('InstrumentList')), null, 2); } - public static toNothing(json: string): Nothing { - return cast(JSON.parse(json), r('Nothing')); + public static toCountry(json: string): Country { + return cast(JSON.parse(json), r('Country')); } - public static nothingToJson(value: Nothing): string { - return JSON.stringify(uncast(value, r('Nothing')), null, 2); + public static countryToJson(value: Country): string { + return JSON.stringify(uncast(value, r('Country')), null, 2); } public static toOrganization(json: string): Organization { @@ -314,20 +198,12 @@ export class Convert { return JSON.stringify(uncast(value, r('Position')), null, 2); } - public static toTimeRange(json: string): TimeRange { - return cast(JSON.parse(json), r('TimeRange')); - } - - public static timeRangeToJson(value: TimeRange): string { - return JSON.stringify(uncast(value, r('TimeRange')), null, 2); - } - - public static toValuation(json: string): Valuation { - return cast(JSON.parse(json), r('Valuation')); + public static toNothing(json: string): Nothing { + return cast(JSON.parse(json), r('Nothing')); } - public static valuationToJson(value: Valuation): string { - return JSON.stringify(uncast(value, r('Valuation')), null, 2); + public static nothingToJson(value: Nothing): string { + return JSON.stringify(uncast(value, r('Nothing')), null, 2); } } @@ -475,60 +351,6 @@ const typeMap: any = { ], 'any' ), - Chart: o( - [ - { json: 'instruments', js: 'instruments', typ: a(r('Instrument')) }, - { json: 'otherConfig', js: 'otherConfig', typ: u(undefined, m('any')) }, - { json: 'range', js: 'range', typ: u(undefined, r('TimeRange')) }, - { json: 'style', js: 'style', typ: u(undefined, r('Style')) }, - { json: 'type', js: 'type', typ: '' }, - { json: 'id', js: 'id', typ: u(undefined, m('')) }, - { json: 'name', js: 'name', typ: u(undefined, '') }, - ], - 'any' - ), - Instrument: o( - [ - { json: 'id', js: 'id', typ: r('InstrumentID') }, - { json: 'type', js: 'type', typ: '' }, - { json: 'name', js: 'name', typ: u(undefined, '') }, - ], - 'any' - ), - InstrumentID: o( - [ - { json: 'BBG', js: 'BBG', typ: u(undefined, '') }, - { json: 'CUSIP', js: 'CUSIP', typ: u(undefined, '') }, - { json: 'FDS_ID', js: 'FDS_ID', typ: u(undefined, '') }, - { json: 'FIGI', js: 'FIGI', typ: u(undefined, '') }, - { json: 'ISIN', js: 'ISIN', typ: u(undefined, '') }, - { json: 'PERMID', js: 'PERMID', typ: u(undefined, '') }, - { json: 'RIC', js: 'RIC', typ: u(undefined, '') }, - { json: 'SEDOL', js: 'SEDOL', typ: u(undefined, '') }, - { json: 'ticker', js: 'ticker', typ: u(undefined, '') }, - ], - '' - ), - TimeRange: o( - [ - { json: 'endTime', js: 'endTime', typ: u(undefined, Date) }, - { json: 'startTime', js: 'startTime', typ: u(undefined, Date) }, - { json: 'type', js: 'type', typ: '' }, - { json: 'id', js: 'id', typ: u(undefined, m('')) }, - { json: 'name', js: 'name', typ: u(undefined, '') }, - ], - 'any' - ), - ChatInitSettings: o( - [ - { json: 'chatName', js: 'chatName', typ: u(undefined, '') }, - { json: 'initMessage', js: 'initMessage', typ: u(undefined, '') }, - { json: 'members', js: 'members', typ: u(undefined, r('ContactList')) }, - { json: 'options', js: 'options', typ: u(undefined, 'any') }, - { json: 'type', js: 'type', typ: 'any' }, - ], - 'any' - ), ContactList: o( [ { json: 'contacts', js: 'contacts', typ: a(r('Contact')) }, @@ -553,75 +375,61 @@ const typeMap: any = { ], '' ), - Country: o( - [ - { json: 'id', js: 'id', typ: r('CountryID') }, - { json: 'type', js: 'type', typ: '' }, - { json: 'name', js: 'name', typ: u(undefined, '') }, - ], - 'any' - ), - CountryID: o( - [ - { json: 'COUNTRY_ISOALPHA2', js: 'COUNTRY_ISOALPHA2', typ: u(undefined, '') }, - { json: 'COUNTRY_ISOALPHA3', js: 'COUNTRY_ISOALPHA3', typ: u(undefined, '') }, - { json: 'ISOALPHA2', js: 'ISOALPHA2', typ: u(undefined, '') }, - { json: 'ISOALPHA3', js: 'ISOALPHA3', typ: u(undefined, '') }, - ], - '' - ), - Currency: o( + Email: o( [ - { json: 'id', js: 'id', typ: r('CurrencyID') }, - { json: 'name', js: 'name', typ: u(undefined, '') }, { json: 'type', js: 'type', typ: '' }, + { json: 'recipients', js: 'recipients', typ: u(undefined, '') }, + { json: 'subject', js: 'subject', typ: u(undefined, '') }, + { json: 'textBody', js: 'textBody', typ: u(undefined, '') }, + { json: 'richTextBody', js: 'richTextBody', typ: u(undefined, '') }, ], 'any' ), - CurrencyID: o([{ json: 'CURRENCY_ISOCODE', js: 'CURRENCY_ISOCODE', typ: u(undefined, '') }], ''), - Email: o( + InstrumentList: o( [ - { json: 'recipients', js: 'recipients', typ: r('RecipientsObject') }, - { json: 'subject', js: 'subject', typ: u(undefined, '') }, - { json: 'textBody', js: 'textBody', typ: u(undefined, '') }, + { json: 'instruments', js: 'instruments', typ: a(r('Instrument')) }, { json: 'type', js: 'type', typ: '' }, { json: 'id', js: 'id', typ: u(undefined, m('')) }, { json: 'name', js: 'name', typ: u(undefined, '') }, ], 'any' ), - RecipientsObject: o( + Instrument: o( [ - { json: 'id', js: 'id', typ: u(undefined, r('RecipientsID')) }, + { json: 'id', js: 'id', typ: r('InstrumentID') }, { json: 'type', js: 'type', typ: '' }, { json: 'name', js: 'name', typ: u(undefined, '') }, - { json: 'contacts', js: 'contacts', typ: u(undefined, a(r('Contact'))) }, ], 'any' ), - RecipientsID: o( + InstrumentID: o( [ - { json: 'email', js: 'email', typ: u(undefined, '') }, + { json: 'BBG', js: 'BBG', typ: u(undefined, '') }, + { json: 'CUSIP', js: 'CUSIP', typ: u(undefined, '') }, { json: 'FDS_ID', js: 'FDS_ID', typ: u(undefined, '') }, + { json: 'FIGI', js: 'FIGI', typ: u(undefined, '') }, + { json: 'ISIN', js: 'ISIN', typ: u(undefined, '') }, + { json: 'PERMID', js: 'PERMID', typ: u(undefined, '') }, + { json: 'RIC', js: 'RIC', typ: u(undefined, '') }, + { json: 'SEDOL', js: 'SEDOL', typ: u(undefined, '') }, + { json: 'ticker', js: 'ticker', typ: u(undefined, '') }, ], '' ), - InstrumentList: o( + Country: o( [ - { json: 'instruments', js: 'instruments', typ: a(r('Instrument')) }, + { json: 'id', js: 'id', typ: r('CountryID') }, { json: 'type', js: 'type', typ: '' }, - { json: 'id', js: 'id', typ: u(undefined, m('')) }, { json: 'name', js: 'name', typ: u(undefined, '') }, ], 'any' ), - Nothing: o( + CountryID: o( [ - { json: 'type', js: 'type', typ: '' }, - { json: 'id', js: 'id', typ: u(undefined, m('')) }, - { json: 'name', js: 'name', typ: u(undefined, '') }, + { json: 'ISOALPHA2', js: 'ISOALPHA2', typ: u(undefined, '') }, + { json: 'ISOALPHA3', js: 'ISOALPHA3', typ: u(undefined, '') }, ], - 'any' + '' ), Organization: o( [ @@ -658,19 +466,5 @@ const typeMap: any = { ], 'any' ), - Valuation: o( - [ - { json: 'CURRENCY_ISCODE', js: 'CURRENCY_ISCODE', typ: u(undefined, '') }, - { json: 'expiryTime', js: 'expiryTime', typ: u(undefined, Date) }, - { json: 'price', js: 'price', typ: u(undefined, 3.14) }, - { json: 'type', js: 'type', typ: '' }, - { json: 'valuationTime', js: 'valuationTime', typ: u(undefined, Date) }, - { json: 'value', js: 'value', typ: 3.14 }, - { json: 'CURRENCY_ISOCODE', js: 'CURRENCY_ISOCODE', typ: 'any' }, - { json: 'id', js: 'id', typ: u(undefined, m('')) }, - { json: 'name', js: 'name', typ: u(undefined, '') }, - ], - 'any' - ), - Style: ['bar', 'candle', 'custom', 'heatmap', 'histogram', 'line', 'mountain', 'pie', 'scatter', 'stacked-bar'], + Nothing: o([{ json: 'type', js: 'type', typ: '' }], 'any'), }; diff --git a/src/context/schemas.json b/src/context/schemas.json index 4310f348d..d3edd5515 100644 --- a/src/context/schemas.json +++ b/src/context/schemas.json @@ -1,7 +1,9 @@ { + "ChatInitSettings": [ + "https://fdc3.finos.org/schemas/next/chatInitSettings.schema.json" + ], "Context": ["https://fdc3.finos.org/schemas/next/context.schema.json"], "Chart": ["https://fdc3.finos.org/schemas/next/chart.schema.json"], - "ChatInitSettings": ["https://fdc3.finos.org/schemas/next/chatInitSettings.schema.json"], "Contact": ["https://fdc3.finos.org/schemas/next/contact.schema.json"], "ContactList": ["https://fdc3.finos.org/schemas/next/contactList.schema.json"], "Country": ["https://fdc3.finos.org/schemas/next/country.schema.json"], @@ -9,10 +11,11 @@ "Email": ["https://fdc3.finos.org/schemas/next/email.schema.json"], "Instrument": ["https://fdc3.finos.org/schemas/next/instrument.schema.json"], "InstrumentList": ["https://fdc3.finos.org/schemas/next/instrumentList.schema.json"], - "Nothing": ["https://fdc3.finos.org/schemas/next/nothing.schema.json"], + "Country": ["https://fdc3.finos.org/schemas/next/country.schema.json"], "Organization": ["https://fdc3.finos.org/schemas/next/organization.schema.json"], "Portfolio": ["https://fdc3.finos.org/schemas/next/portfolio.schema.json"], "Position": ["https://fdc3.finos.org/schemas/next/position.schema.json"], + "Nothing": ["https://fdc3.finos.org/schemas/next/nothing.schema.json"], "TimeRange": ["https://fdc3.finos.org/schemas/next/timerange.schema.json"], "Valuation": ["https://fdc3.finos.org/schemas/next/valuation.schema.json"] } diff --git a/test/Methods.test.ts b/test/Methods.test.ts index 62388aaad..3584de0d2 100644 --- a/test/Methods.test.ts +++ b/test/Methods.test.ts @@ -12,7 +12,6 @@ import { findIntentsByContext, getCurrentChannel, getInfo, - getAppMetadata, getOrCreateChannel, getUserChannels, getSystemChannels, @@ -275,14 +274,6 @@ describe('test ES6 module', () => { expect(window.fdc3.getInfo).toHaveBeenCalledTimes(1); expect(window.fdc3.getInfo).toHaveBeenCalledWith(); }); - - test('getAppMetadata should delegate to window.fdc3.getAppMetadata', async () => { - const dummyApp = { appId: 'dummy' }; - await getAppMetadata(dummyApp); - - expect(window.fdc3.getAppMetadata).toHaveBeenCalledTimes(1); - expect(window.fdc3.getAppMetadata).toHaveBeenCalledWith(dummyApp); - }); }); describe('fdc3Ready', () => { diff --git a/toolbox/fdc3-workbench/yarn.lock b/toolbox/fdc3-workbench/yarn.lock index 04007b9a9..55248a8ff 100644 --- a/toolbox/fdc3-workbench/yarn.lock +++ b/toolbox/fdc3-workbench/yarn.lock @@ -10389,9 +10389,9 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: urix "^0.1.0" source-map-support@^0.5.6, source-map-support@~0.5.12, source-map-support@~0.5.19: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + version "0.5.20" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" + integrity sha512-n1lZZ8Ve4ksRqizaBQgxXDgKwttHDhyfQjA6YZZn8+AroHbsIz+JjwxQDxbp+7y5OYCI8t1Yk7etjD9CRd2hIw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -10863,9 +10863,9 @@ terser-webpack-plugin@^1.4.3: worker-farm "^1.7.0" terser@^4.1.2, terser@^4.6.2, terser@^4.6.3: - version "4.8.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" - integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== dependencies: commander "^2.20.0" source-map "~0.6.1" diff --git a/website/README.md b/website/README.md index 512b6263f..0ecf96b76 100644 --- a/website/README.md +++ b/website/README.md @@ -16,7 +16,6 @@ This website was created with [Docusaurus](https://docusaurus.io/). # Install dependencies $ yarn ``` - 2. Run your dev server: ```sh @@ -51,11 +50,11 @@ my-docusaurus/ # Versioning -Docusaurus uses the `docusaurus-version` command to create a snapshot of the documents in the `docs` folder with a particular version number, -and places them in the `versioned_docs/version-` folder. It also creates a `versioned_sidebars/version--sidebars.json` -to save the navigation structure at the time the snapshot was taken. +Docusaurus uses the `docusaurus-version` command to create a snapshot of the documents in the `docs` folder with a particular version number, +and places them in the `versioned_docs/version-` folder. It also creates a `versioned_sidebars/version--sidebars.json` +to save the navigation structure at the time the snapshot was taken. -See for more info. +See https://docusaurus.io/docs/en/versioning for more info. ## Versioning scheme @@ -68,26 +67,22 @@ Before creating a version, please make sure docs/fdc3-intro.md has been updated Since the website also uses some generated and copied static files (like schemas), extra tasks need to be performed as part of creating a new version. To create a new version, use this command: - ```sh VERSION= yarn run version ``` - e.g. - ```sh VERSION=1.2 yarn run version ``` The `VERSION` environment variable and `version` script are used to: -* Run the `docusaurus-version` command -* Copy schemas from the `/website/static/schemas/next` (which matches `master`) to `/website/static/schemas/` -* Copy the app-directory OpenAPI html file from `/website/pages/schemas/next` to `/website/pages/schemas/` -* Update paths referring to `/schemas/next` to point to `/schemas/` -* Update the version number in the app directory schema from `version: next` to `version: ` +- Run the `docusaurus-version` command +- Copy schemas from the `/website/static/schemas/next` (which matches `master`) to `/website/static/schemas/` +- Copy the app-directory OpenAPI html file from `/website/pages/schemas/next` to `/website/pages/schemas/` +- Update paths referring to `/schemas/next` to point to `/schemas/` +- Update the version number in the app directory schema from `version: next` to `version: ` After a new version is created with the script, the following step also needs to be performed: - 1. Change `defaultVersionShown` in `siteConfig.js` to match the latest version (if the new version is now the latest version). 2. Change `versions.json` to have the version `stable` at the top, followed by the actual version numbers in descending order, e.g. `[stable, 1.3, 1.2, 1.1]`. (Docusaurus will add the new version number at the top of the array.) @@ -96,13 +91,10 @@ These steps are needed because we follow a workaround for an [issue with permane ## Delete a version To delete a version, use this command: - ```sh VERSION= yarn run version:delete ``` - e.g. - ```sh VERSION=1.2 yarn run version:delete ``` @@ -133,7 +125,6 @@ For more information about docs, click [here](https://docusaurus.io/docs/en/navi Edit blog posts by navigating to `website/blog` and editing the corresponding post: `website/blog/post-to-be-edited.md` - ```markdown --- id: post-needs-edit @@ -183,7 +174,6 @@ For more information about adding new docs, click [here](https://docusaurus.io/d 1. Make sure there is a header link to your blog in `website/siteConfig.js`: `website/siteConfig.js` - ```javascript headerLinks: [ ... @@ -214,7 +204,6 @@ For more information about blog posts, click [here](https://docusaurus.io/docs/e 1. Add links to docs, custom pages or external links by editing the headerLinks field of `website/siteConfig.js`: `website/siteConfig.js` - ```javascript { headerLinks: [ @@ -239,7 +228,6 @@ For more information about the navigation bar, click [here](https://docusaurus.i 1. If you want your page to show up in your navigation header, you will need to update `website/siteConfig.js` to add to the `headerLinks` element: `website/siteConfig.js` - ```javascript { headerLinks: [ @@ -255,7 +243,7 @@ For more information about custom pages, click [here](https://docusaurus.io/docs ## Custom CSS & Design Changes -1. Changing logos for the Header and Footer and Favicon are done in website/siteConfig.js file in the /*path to images for header/footer*/ section +1. Changing logos for the Header and Footer and Favicon are done in website/siteConfig.js file in the /* path to images for header/footer */ section Note: make sure that you add your new logos to the website/static/img folder first. @@ -265,7 +253,7 @@ Note: make sure that you add your new logos to the website/static/img folder fir 3. Change the background color and the background transparent image in the website/static/css/custom.css file. -Go to the section labeled: .homeContainer +Go to the section labeled: .homeContainer Change "background-image" - and insert your new background image file. Note - make sure you add your new background transparent image file to the website/static/img folder first. diff --git a/website/data/community.json b/website/data/community.json deleted file mode 100644 index 22df5caf0..000000000 --- a/website/data/community.json +++ /dev/null @@ -1,312 +0,0 @@ -[ - { - "//": "Note: compliance testing information is likely to be required for listing platform providers (at least) on the FDC3 website in future" - }, - { - "title": "ChartIQ", - "publisher": "Cosaic, Inc.", - "image": "/img/users/ChartIQ.png", - "infoLink": "https://cosaic.io/chartiq/", - "docsLink": "https://documentation.chartiq.com/", - "type": "application-provider", - "badges": [ - { - "text": "FDC3 1.2 Supported" - } - ], - "description": "

ChartIQ is a powerful and flexible HTML5 charting library with millions of users worldwide. Written in JavaScript, it runs entirely within the browser. With state of the art integrations, including third-party apps, educational components, analysis, and more, you can create efficient workflows and streamline the way to work.

ChartIQ works on any platform (mobile, web, desktop) and any framework (Angular, React).

Most companies have at least one financial charting library for each platform or app they target—web, C#, Java, Android, iOS, etc., so developers have multiple code bases to maintain. With ChartIQ, write your code once and use it everywhere.

" - }, - { - "title": "Electron-FDC3", - "publisher": "FINOS", - "image": "/img/users/fdc3-electron.png", - "infoLink": "https://github.com/finos/electron-fdc3", - "docsLink": "https://github.com/finos/electron-fdc3", - "type": "platform-provider", - "badges": [ - { - "text": "FDC3 1.2 Supported" - }, - { - "text": "Open Source" - } - ], - "description": "

This project provides a fully open source implementation of the FDC3 interoperability standard.

Includes a fully featured and secure electron desktop agent featuring intent resolution, channel linking, directory search and a local file-based app directory implementation.

" - }, - { - "title": "FDC3 Desktop Agent Chrome Extension", - "publisher": "FINOS", - "image": "/img/users/fdc3-desktop-agent.png", - "infoLink": "https://github.com/finos/fdc3-desktop-agent", - "docsLink": "https://github.com/finos/fdc3-desktop-agent", - "type": "platform-provider", - "badges": [ - { - "text": "FDC3 1.2 Supported" - }, - { - "text": "Open Source" - } - ], - "description": "

The FDC3 Desktop Agent is an open source implementation of FDC3 as a Chrome Extension.

Its purpose is to provide a quick and easy way for app developers to get started with the FDC3 APIs.

" - }, - { - "title": "Finsemble", - "publisher": "Cosaic, Inc.", - "image": "/img/users/Finsemble.png", - "infoLink": "https://cosaic.io/finsemble/", - "docsLink": "https://documentation.finsemble.com/", - "type": "platform-provider", - "badges": [ - { - "text": "FDC3 1.2 Supported" - } - ], - "description": "

Finsemble is a no code/low code smart desktop platform that helps you achieve both visual and logical integration of various types of apps that you already use (web, native, in-house, and third-party, including Citrix virtual apps).

Connect apps into automated workflows to improve user efficiency and reduce error rates. With SelectConnect, you are in full control over which apps communicate and which are excluded.

Build a fully customizable UI to work across multiple windows and monitors.

Use our Smart Desktop Designer to create and share fully functioning, highly customized, integrated desktops in a few hours, no coding knowledge needed.

" - }, - { - "title": "FDC3 Workbench", - "publisher": "FDC3 / FINOS", - "image": "/toolbox/fdc3-workbench/fdc3-icon-256.png", - "infoLink": "http://fdc3.finos.org/toolbox/fdc3-workbench", - "docsLink": "https://github.com/finos/FDC3/blob/master/toolbox/fdc3-workbench/README.md", - "type": "examples-and-training", - "badges": [], - "description": "

When developing an FDC3-compliant app or desktop agent, you need to test. Because FDC3 is about communicating, you need at least one other app to communicate with. You could grab an existing app, but there may not be one available that uses the messaging you need to test.

Many developers end up writing their own helper tool that they discard when their app is done. So many devs have created these throwaway apps that the Finsemble team @ Cosaic decided to build and contribute a workbench (for any FDC3-compliant platform) that helps develop and test your app without writing throwaway code.

" - }, - { - "title": "FDC3 eXplained", - "publisher": "FDC3 / FINOS", - "image": "/toolbox/fdc3-explained/logo.png", - "infoLink": "https://fdc3.finos.org/toolbox/fdc3-explained", - "docsLink": "https://github.com/finos/FDC3/blob/master/toolbox/fdc3-explained/README.md", - "type": "examples-and-training", - "badges": [], - "description": "FDC3 eXplained is a no-frills tool for learning more about FDC3 concepts without requiring any engineering or coding knowledge. Click the buttons and see the API response, type in context and broadcast it. No dependencies, libraries or frameworks - just a single html page per version that you can load in any environment. Similar to FDC3 Workbench it provides a reference implementation for you to test and verify the FDC3 functionality of other applications and desktop agents." - }, - { - "title": "FDC3: Interoperability for the Financial Desktop (training course)", - "publisher": "LinuxFoundationX", - "image": "https://linuxfoundation.org/wp-content/uploads/Linux-Foundation-OG-Image.png", - "infoLink": "https://www.edx.org/course/fdc3-interoperability-for-the-financial-desktop", - "type": "examples-and-training", - "badges": [], - "description": "The FDC3 standard for application interoperability has become a key fintech enabler for transformation as financial organizations embrace micro-frontend-oriented workflows. Learn about the vision and key concepts of FDC3, the benefits it provides, and how workflow-driven design makes it easy to get started with FDC3." - }, - { - "title": "Glue42 Enterprise", - "publisher": "Glue42", - "image": "/img/users/Glue42 Enterprise Positive.svg", - "infoLink": "https://glue42.com/enterprise/", - "docsLink": "https://docs.glue42.com/", - "type": "platform-provider", - "badges": [ - { - "text": "FDC3 1.2 Supported" - } - ], - "description": "

Glue42 enables organizations to build intelligent desktops that support configurable workflows between web and desktop applications. We’re working with some of the world’s biggest financial organizations to help them optimize complex processes and become more efficient.

Our flagship product, Glue42 Enterprise, is a desktop application integration platform that helps organizations create a simplified desktop experience by connecting the UI and data of any application, e.g., legacy, in-house or web. The platform supports open-source standards, including FINOS-FDC3, thus reducing delivery time and avoiding vendor lock-in.

" - }, - { - "title": "OpenFin", - "publisher": "OpenFin Inc.", - "image": "/img/users/Openfin.png", - "infoLink": "https://openfin.co/", - "docsLink": "https://developers.openfin.co/of-docs", - "type": "platform-provider", - "badges": [], - "description": "

OpenFin founded the FDC3 standard in 2017 in collaboration with major banks and asset managers. Deployed at more than 2400 financial firms, OpenFin OS, allows applications from multiple providers to co-exist and interoperate on the desktop.

OpenFin OS consists of a secure, Chromium-based runtime called Container and a visual interface called Workspace. Workspace gets you running in no time, with themeable UI components for complex windowing, advanced search, actionable notifications and application discovery.

You can also build your own solutions using the OpenFin Container, native language adapters and APIs while remaining compatible with everything else built on OpenFin.

" - }, - { - "title": "FDC3 Fast and Easy: Introducing the FDC3 Workbench for Vendors", - "publisher": "The Linux Foundation", - "image": "/img/community/lf.png", - "infoLink": "https://www.youtube.com/watch?v=JSEAC_1rku8", - "description": "Kris West (Cosaic) introduces FDC3 and the FDC3 Workbench at the October 2021 Open Source Strategy Forum in London. ", - "badges": [], - "type": "examples-and-training" - }, - { - "title": "Building an FDC3-enabled Web Application", - "publisher": "The Linux Foundation", - "image": "/img/community/lf.png", - "infoLink": "https://www.youtube.com/watch?v=BJvbVeL7ZJA", - "description": "Riko Eksteen (Adaptive Financial Consulting) explains why and how to build an FDC3-enabled application at the December 2020 Open-Source Strategy Forum", - "badges": [], - "type": "examples-and-training" - }, - { - "title": "Symphony Demo - FDC3", - "publisher": "Symphony", - "image": "/img/users/Symphony.png", - "infoLink": "https://www.youtube.com/watch?v=wMF1h7RaAJY", - "description": "This demo showcases Symphony’s interoperability, flexibility and open architecture via FDC3 protocol. Leveraging FDC3, data and information can be seamlessly shared across multiple platforms on the same desktop. In this demo we showcase FX liquidity pricing via Tradefeedr, and data and trading analytics via Cosaic’s ChartIQ and Finsemble.", - "badges": [], - "type": "examples-and-training" - }, - { - "title": "FDC3: A Review of Industry Activity - Matt Barrett & Sachin Gaba", - "publisher": "Linux Foundation", - "image": "/img/community/lf.png", - "infoLink": "https://www.youtube.com/watch?v=w1d0d1nM8iI", - "description": "9 Oct 2021 FDC3: A Review of Industry Activity - Matt Barrett, Adaptive Financial Consulting & Sachin Gaba, State Street. “Since we last gathered in person, FDC3 usage has seen incredible growth. This talk will provide an overview of FDC3 usage across the industry, highlighting both internal and external usage. We will cover high-impact FDC3 usage and identify some key themes and trends. I’ll share some predictions about the next year of FDC3 growth, and identify missed opportunities.", - "badges": [], - "type": "examples-and-training" - }, - { - "title": "FDC3 Google Groups", - "publisher": "Google Groups", - "image": "/img/community/groups.png", - "infoLink": "https://groups.google.com/a/finos.org/g/fdc3", - "description": "A (fairly active) Google group where you can get involved with the FDC3 project. Talk with implementers, ask questions, find answers and hear the latest news on the FDC3 project.", - "badges": [], - "type": "examples-and-training" - }, - { - "title": "S&P Front Office", - "publisher": "S&P Global Market Intelligence", - "image": "/img/users/spglobal.png", - "infoLink": "https://www.spglobal.com/marketintelligence/en/client-segments/investment-management#portfolio-management", - "description": "As a provider of buyside Front Office applications, we are advocating for and pushing the standard forward. We intend to utilize the protocol(s) within our next-generation Investment Management platform in order to power both S&P Global and third-party desktop app integrations.", - "badges": [], - "type": "application-provider" - }, - { - "title": "Norman & Sons", - "publisher": "Norman & Sons", - "image": "/img/users/norman-and-sons.png", - "infoLink": "https://www.normanandsons.com/", - "docsLink": "https://www.normanandsons.com/research/", - "description": "Norman & Sons is a digital product studio specializing in the research, design and build of user-centered enterprise applications. The studio has led the design and delivery of some of the best-known enterprise digital platforms for world-leading financial services firms and product firms.

They are starting to contribute to the development of Electron FDC3.", - "badges": [], - "type": "solution-provider" - }, - { - "title": "Ignite UI", - "publisher": "Infragistics", - "image": "https://static.infragistics.com/marketing/Website/General/Infragistics-horizontal.svg", - "infoLink": "https://www.infragistics.com/openfin", - "docsLink": "https://www.npmjs.com/package/igniteui-webcomponents-fdc3", - "description": "Ignite UI from Infragistics is a feature complete UI Platform for FDC3-enabled financial services apps. With over 60 UI controls & components plus charting including the world’s fastest virtualized data grids & data charts. No-lag, smooth scrolling for unlimited columns & rows of data, real-time, high-volume data support, with support for Web Components, Angular, React & Blazor, you have all the UX features you need for your modern FinTech apps.\nWith our FDC3 Data Adapter, build low-touch, composite-based financial services desktop workflows with FDC3 and give your apps the chance to participate in the broader FinTech ecosystem.", - "badges": [], - "type": "application-provider" - }, - { - "title": "Client Engagement and Data Driven Advisory", - "publisher": "Singletrack", - "image": "https://www.singletrack.com/wp-content/uploads/2021/03/singletrack-logo-1.svg", - "infoLink": "https://www.singletrack.com/sell-side/", - "docsLink": "https://www2.singletrack.com/sell-side-product-overview", - "description": "Singletrack uses FDC3 to provide smooth desktop workflows for Sales, Research and Corporate Access professionals as they move between tools such as Chat & Email and platforms such as Order Management, Authoring & our own to deliver their daily work. With FDC3 we increase efficiency and ensure rich, relevant information is always at hand.", - "badges": [], - "type": "application-provider" - }, - { - "title": "Scott Logic Financial Services", - "publisher": "Scott Logic", - "image": "/img/users/scottlogic.png", - "infoLink": "https://www.scottlogic.com/", - "docsLink": "", - "description": "Scott Logic is a UK-based software consultancy that provides software development services to a number of financial services companies, many of which are FINOS members. We have built numerous bespoke, feature-rich and high-end web-based desktop solutions for our clients, including trading platforms, risk analytics and regulatory reporting. This has given us a wealth of experience of desktop container technologies and desktop interoperability. We use FDC3 with numerous clients.\nAs active FINOS members, we are keen supporters of FDC3, and have recently created the FDC3 conformance framework which automates the process of ensuring the FDC3 APIs are faithfully implemented.", - "badges": [], - "type": "solution-provider" - }, - { - "title": "Provide Adaptable", - "publisher": "Adaptable Tools", - "image": "https://uploads-ssl.webflow.com/617a7f56602ce8b1af560594/620514d55860251a1520d1b9_Logo-AdaptableTools-text-light%20(1).svg", - "infoLink": "https://www.adaptabletools.com/", - "docsLink": "https://docs.adaptabletools.com/", - "description": "A powerful AG Grid add-on which includes FDC3 columns allowing users to broadcast FDC3 messages and raise FDC3 intents.

We provide FDC3 columns allowing developers using AdapTable to build applications that can broadcast FDC3 messages and raise FDC3 intents. When using AdapTable inside OpenFin, Finsemble or Glue42 we enable communication between widgets using FDC3.", - "badges": [], - "type": "application-provider" - }, - { - "title": "ipushpull", - "publisher": "Pushpull Technology Ltd", - "image": "/img/users/ipp-logo.png", - "infoLink": "https://ipushpull.com/", - "docsLink": "https://support.ipushpull.com/", - "description": "ipushpull is a configurable low & no-code platform that makes it easy to maximise the value of your data-driven services. We help you to transform your clients’ experience with our low-code custom applications & solutions and integrated, white-labelled workflows.

The ipushpull platform seamlessly integrates with all desktop interop solutions and lets you build your own FDC3-driven workflows through configuration alone.", - "badges": [], - "type": "application-provider" - }, - { - "title": "Symphony", - "publisher": "Symphony", - "image": "/img/users/Symphony.png", - "infoLink": "https://symphony.com/", - "docsLink": "https://docs.developers.symphony.com/embedded-modules/desktop-interoperability", - "description": "Symphony is the most secure and compliance-enabling markets’ infrastructure and technology platform, where solutions are built or integrated to standardize, automate and innovate financial services workflows. It is a vibrant community of over half a million financial professionals with a trusted directory and serves over 1,000 institutions.

Symphony is powering over 2,000 community built applications and bots.

The company's mission is to promote a strong ecosystem of innovation by our customers and partners on the platform. As a result, the platform has been built with an open API approach to benefit our end users through the fostering of deep integrations and novel workflows.

Specifically, Symphony considers Desktop Integration Platforms integration as a key workflow enabler, bringing benefits like cross-application workflows and context sharing, better screen real-estate management and aggregated notification centers. As such, the company is actively working with the FINOS FDC3 working group to standardize DIP integration APIs across the industry.", - "badges": [], - "type": "application-provider" - }, - { - "title": "UBS uses FDC3 to build their next generation Derivatives Sales and Trading Desktop", - "publisher": "UBS AG", - "image": "/img/users/ubs.png", - "infoLink": "https://ubs.com/", - "description": "UBS Investment Bank are developing their next generation of Derivatives Sales and Trading Desktop using FDC3 from the ground up.

Contributions are predominantly via FDC3 related meetings and discussions around the standards, contexts and intents.", - "badges": [], - "type": "adopter" - }, - { - "title": "Reactive Trader", - "publisher": "Adaptive Financial Consulting", - "image": "https://www.reactivetrader.com/static/media/reactive-trader-icon-256x256.png", - "infoLink": "https://www.reactivetrader.com/", - "docsLink": "https://weareadaptive.com/showcase/", - "description": "Reactive Trader® is Adaptive’s showcase FX trading application, built with modern web technologies, and available as open source. It showcases integration with multiple different desktop platforms, including PWA, OpenFin, Finsemble, Glue42 and Symphony. Reactive Trader® broadcasts out instrument messages via FDC3 when currency pairs are selected in the blotter. It can interoperate with Reactive Analytics.", - "badges": [{"text": "FDC3 1.2 Supported"}, {"text": "Open Source"}], - "type": "examples-and-training" - }, - { - "title": "Reactive Analytics", - "publisher": "Adaptive Financial Consulting", - "image": "https://demo-reactive-analytics.adaptivecluster.com/static/media/reactive-analytics-icon-256x256.png", - "infoLink": "https://demo-reactive-analytics.adaptivecluster.com/ ", - "docsLink": " https://github.com/AdaptiveConsulting/ReactiveAnalytics#readme", - "description": "Reactive Analytics is an Adaptive’s example stock and FX analytics application, built with modern web technologies, and available as open source. It showcases integration with multiple different desktop platforms, including PWA, OpenFin and Finsemble. Reactive Analytics listens and responds to instrument broadcast messages from other applications via FDC3, and can interoperate with Reactive Trader. ", - "badges": [{"text": "FDC3 1.2 Supported"}, {"text": "Open Source"}], - "type": "examples-and-training" - }, - { - "title": "Consulting Services", - "publisher": "Adaptive Financial Consulting", - "image": "https://fdc3.finos.org/img/users/adaptive.png", - "infoLink": "https://demo-reactive-analytics.adaptivecluster.com/ ", - "docsLink": " https://weareadaptive.com/clients-stories/#electronic-trading-with-a-personal-touch", - "description": "This case study dives into how Adaptive was able to utilise FDC3 to deliver all the benefits of an electronic trading platform with full MiFID II compliance, while providing clients with the advantages of high-touch voice trading.", - "badges": [], - "type": "solution-provider" - }, - { - "title": "Pictet AM smart Desktop", - "publisher": "Pictet Asset Management", - "image": "/img/users/pictet.png", - "infoLink": "https://am.pictet", - "description": "Pictet AM smart Desktop - linking our internal PMS (proprietary), our OMS (Charles River IMS) and various in-house react.js applications implementing FDC3 to share context and intents to provide a streamlined experience for Traders and Fund managers.", - "badges": [], - "type": "adopter" - }, - { - "title": "Connectifi", - "publisher": "Connectifi Co.", - "image": "/img/users/connectifi.png", - "infoLink": "https://www.connectifi.co", - "description": "Connectifi provides FDC3 interop as a cloud service, empowering organizations to connect anything, anywhere, in a security-first environment that does not require installs.\n\nUse Connectifi to leverage FDC3 in standard browser and mobile environments, to expand the reach and ROI of your existing desktop tech stack, and to create secure and verifiable interop channels between mission critical applications.", - "badges": [{"text": "FDC3 1.2 Supported"}], - "type": "platform-provider" - }, - { - "title": "FlexTrade Buy and Sell-Side O/EMS Solutions", - "publisher": "FlexTrade Systems", - "image": "/img/users/flextrade.jpg", - "infoLink": "https://flextrade.com/buy-side-flextrade-products/", - "docsLink": "https://flextrade.com/openfin/", - "description": "FlexTrade uses the FDC3 standard for application interoperability to deliver trading teams a seamless experience across its full suite of buy and sell-side O/EMS solutions. Using FDC3 within FlexTrade’s solutions, trading teams are empowered to choose to customize their workflow with the specific tools and data unique to their needs. Further, it is visualized into a single view within their order blotter, eliminating the need to context switch between different screens or solutions within their workflow, enabling better decision-making and improving trading performance.

The work with OpenFin and LSEG Turquoise recently picked up “Financial Technology Innovation of the Year” in the Financial News Excellence in Trading and Tech Awards 2022. ", - "badges": [], - "type": "application-provider" - } - -] \ No newline at end of file diff --git a/website/data/implementations.json b/website/data/implementations.json new file mode 100644 index 000000000..0bf875f72 --- /dev/null +++ b/website/data/implementations.json @@ -0,0 +1,53 @@ +[ + { "//": "Implementations page content - required fields: title, image, type, description", + "//": "Image link may be external or images may be added to the /website/static/img/users directory for hosting on the FDC3 website", + "//": "Please keep descriptions to (at most) 100 words.", + "//": "Description content may be formatted in basic HTML (e.g.

, and tags)", + "//": "Please do not add complex structure, additional images or more than 3 inline links or your PR may be rejected", + "//": "Note: compliance testing information is likely to be required for listing platform providers (at least) on the FDC3 website in future" + }, + { + "title": "ChartIQ", + "publisher": "Cosaic, Inc.", + "image": "/img/users/ChartIQ.png", + "infoLink": "https://cosaic.io/chartiq/", + "docsLink": "https://documentation.chartiq.com/", + "type": "application-provider", + "description": "

ChartIQ is a powerful and flexible HTML5 charting library with millions of users worldwide. Written in JavaScript, it runs entirely within the browser. With state of the art integrations, including third-party apps, educational components, analysis, and more, you can create efficient workflows and streamline the way to work.

ChartIQ works on any platform (mobile, web, desktop) and any framework (Angular, React).

Most companies have at least one financial charting library for each platform or app they target—web, C#, Java, Android, iOS, etc., so developers have multiple code bases to maintain. With ChartIQ, write your code once and use it everywhere.

" + }, + { + "title": "Finsemble", + "publisher": "Cosaic, Inc.", + "image": "/img/users/Finsemble.png", + "infoLink": "https://cosaic.io/finsemble/", + "docsLink": "https://documentation.finsemble.com/", + "type": "platform-provider", + "description": "

Finsemble is a no code/low code smart desktop platform that helps you achieve both visual and logical integration of various types of apps that you already use (web, native, in-house, and third-party, including Citrix virtual apps).

Connect apps into automated workflows to improve user efficiency and reduce error rates. With SelectConnect, you are in full control over which apps communicate and which are excluded.

Build a fully customizable UI to work across multiple windows and monitors.

Use our Smart Desktop Designer to create and share fully functioning, highly customized, integrated desktops in a few hours, no coding knowledge needed.

" + }, + { + "title": "FDC3 Workbench", + "publisher": "FDC3 / FINOS", + "image": "/toolbox/fdc3-workbench/fdc3-icon-256.png", + "infoLink": "http://fdc3.finos.org/toolbox/fdc3-workbench", + "docsLink": "https://github.com/finos/FDC3/blob/master/toolbox/fdc3-workbench/README.md", + "type": "examples-and-training", + "description": "

When developing an FDC3-compliant app or desktop agent, you need to test. Because FDC3 is about communicating, you need at least one other app to communicate with. You could grab an existing app, but there may not be one available that uses the messaging you need to test.

Many developers end up writing their own helper tool that they discard when their app is done. So many devs have created these throwaway apps that the Finsemble team @ Cosaic decided to build and contribute a workbench (for any FDC3-compliant platform) that helps develop and test your app without writing throwaway code.

" + }, + { + "title": "FDC3 eXplained", + "publisher": "FDC3 / FINOS", + "image": "/toolbox/fdc3-explained/logo.png", + "infoLink": "https://fdc3.finos.org/toolbox/fdc3-explained", + "docsLink": "https://github.com/finos/FDC3/blob/master/toolbox/fdc3-explained/README.md", + "type": "examples-and-training", + "description": "FDC3 eXplained is a no-frills tool for learning more about FDC3 concepts without requiring any engineering or coding knowledge. Click the buttons and see the API response, type in context and broadcast it. No dependencies, libraries or frameworks - just a single html page per version that you can load in any environment. Similar to FDC3 Workbench it provides a reference implementation for you to test and verify the FDC3 functionality of other applications and desktop agents." + }, + { + "title": "FDC3: Interoperability for the Financial Desktop (training course)", + "publisher": "LinuxFoundationX", + "image": "https://linuxfoundation.org/wp-content/uploads/Linux-Foundation-OG-Image.png", + "infoLink": "https://www.edx.org/course/fdc3-interoperability-for-the-financial-desktop", + "type": "examples-and-training", + "description": "The FDC3 standard for application interoperability has become a key fintech enabler for transformation as financial organizations embrace micro-frontend-oriented workflows. Learn about the vision and key concepts of FDC3, the benefits it provides, and how workflow-driven design makes it easy to get started with FDC3." + } +] diff --git a/website/data/pending-community.json b/website/data/pending-community.json deleted file mode 100644 index 367c1bd9e..000000000 --- a/website/data/pending-community.json +++ /dev/null @@ -1,12 +0,0 @@ - - { - "title": "Open Source Fraud Risk Management (Transaction Monitoring)", - "publisher": "Tecnknowmage.com", - "image": "", - "infoLink": "https://github.com/ActioFRM", - "docsLink": "https://github.com/ActioFRM", - "description": "Recognizing the need, the Bill & Melinda Gates Foundation funded the development of an MVP Open-Source Transactional Monitoring System. The goals were:\nTo continue the previously funded work around anti-fraud in mobile money systems and the typologies and assessment models developed in partnership with Deloitte and others.\nBuild an effective and efficient API driven transactional monitoring engine that can be consumed in full or in part by any organization desiring to consume it and build around it.\nRun large or small anti-fraud programs.\nThe system must be state of the art and use only open-source components. MVP is complete. Onward!", - "badges": [], - "type": "application-provider" - } - ] \ No newline at end of file diff --git a/website/data/users.json b/website/data/users.json index b6f30188d..e563ece50 100644 --- a/website/data/users.json +++ b/website/data/users.json @@ -55,9 +55,9 @@ "isMember": true }, { - "caption": "S&P Global", - "image": "/img/users/spglobal.png", - "infoLink": "https://spglobal.com/", + "caption": "IHS Markit", + "image": "/img/users/IHSMarkit.png", + "infoLink": "https://ihsmarkit.com/", "pinned": true, "isMember": true }, @@ -103,53 +103,11 @@ "pinned": true, "isMember": true }, - { - "caption": "Norman & Sons", - "image": "/img/users/norman-and-sons.png", - "infoLink": "https://www.normanandsons.com/", - "pinned": true, - "isMember": true - }, - { - "caption": "iPushPull", - "image": "/img/users/ipp-logo.png", - "infoLink": "https://www.ipushpull.com/", - "pinned": true, - "isMember": true - }, { "caption": "Symphony", "image": "/img/users/Symphony.png", "infoLink": "https://www.symphony.com/", "pinned": true, "isMember": true - }, - { - "caption": "Pictet", - "image": "/img/users/pictet.png", - "infoLink": "https://www.pictet.com/", - "pinned": true, - "isMember": true - }, - { - "caption": "UBS", - "image": "/img/users/ubs.png", - "infoLink": "https://www.ubs.com/", - "pinned": true, - "isMember": true - }, - { - "caption": "Connectifi", - "image": "/img/users/connectifi.png", - "infoLink": "https://www.connectifi.co/", - "pinned": true, - "isMember": true - }, - { - "caption": "FlexTrade", - "image": "/img/users/flextrade.jpg", - "infoLink": "https://www.flextrade.com/", - "pinned": true, - "isMember": true - } + } ] diff --git a/website/pages/en/get-involved.js b/website/pages/en/get-involved.js index c7e795956..0c5a1bfe2 100644 --- a/website/pages/en/get-involved.js +++ b/website/pages/en/get-involved.js @@ -53,7 +53,7 @@ function Help(props) { content: `If you are an existing user of the FDC3 Standard, we would love to hear from you: just email the [FDC3 General List](fdc3@finos.org) with details about how you are using it. - If you'd like to be listed as a user on our [homepage](https://fdc3.finos.org), + If you'd like to be listed as a user on our [homepage](fdc3.finos.org), you can directly [send a pull request](https://github.com/finos/FDC3/edit/master/website/data/users.json) or, [contact us](fdc3-private@finos.org) if you need help with legal evaluation of your logo.`, }, diff --git a/website/pages/en/community.js b/website/pages/en/implementations.js similarity index 70% rename from website/pages/en/community.js rename to website/pages/en/implementations.js index 900722b34..8853a1da0 100644 --- a/website/pages/en/community.js +++ b/website/pages/en/implementations.js @@ -10,17 +10,7 @@ const React = require('react'); const { useState, useEffect } = React; const CompLibrary = require('../../core/CompLibrary.js'); const Container = CompLibrary.Container; -const implData = require(`${process.cwd()}/data/community.json`); - -const badgeTitles = { - "Open Source": "Indicates that the project source code is available to download and modify, under an Apache 2.0 or similar license.", - "FDC3 1.2 Supported": "Indicates that this product advertises compatibility with the FDC3 1.2 Standard. ", - "FDC3 2.0 Supported ": "Indicates that this product advertises compatibility with the FDC3 2.0 Standard. ", - "FDC3 1.2 Compliant": "This badge is applied to desktop agents that have passed the FINOS FDC3 1.2 Conformance testing process.", - "FDC3 2.0 Compliant": "This badge is applied to desktop agents that have passed the FINOS FDC3 2.0 Conformance testing process.", - "FDC3 2.0 Support Coming Soon": "This product is working towards attaining the FDC3 2.0 Standard.", -} - +const implData = require(`${process.cwd()}/data/implementations.json`); //remove comments implData.forEach(function (item, index, object) { @@ -42,7 +32,7 @@ implData.sort((a, b) => { } }); -function Implementation({ type, title, publisher, image, infoLink, docsLink, badges, description }) { +function Implementation({ type, title, publisher, image, infoLink, docsLink, description }) { return
@@ -60,9 +50,6 @@ function Implementation({ type, title, publisher, image, infoLink, docsLink, bad {docsLink ? Documentation : null}
-
- { badges.map(b =>{b.text})} -
@@ -79,15 +66,9 @@ function ImplementationsShowcase() { - - @@ -103,16 +84,16 @@ function ImplementationsShowcase() { function Implementations(props) { const { config: siteConfig } = props; const { repoUrl } = siteConfig; - const editUrl = `https://www.finos.org/get-involved-fdc3`; + const editUrl = `${repoUrl}/edit/master/website/data/implementations.json`; return -

FDC3 Community

+

FDC3 Implementations

The Financial Desktop Connectivity and Collaboration Consortium (FDC3) standard is maintained and used by leading organizations across the financial industry through a variety of different implementations.

- For more detail on who's implementing the Desktop Agent (a "Platform Provider"), using FDC3 to enable interop with their apps (an "App Provider") or details on where to find tools, examples apps and training materials see below. + For more detail on who's implementing the Desktop Agent (a "Platform Provider"), using FDC3 to enable interop with their apps (an "Application Provider") or details on where to find tools, examples apps and training materials see below.

diff --git a/website/pages/en/index.js b/website/pages/en/index.js index f8ad41778..e9c85727d 100644 --- a/website/pages/en/index.js +++ b/website/pages/en/index.js @@ -71,7 +71,6 @@ class HomeSplash extends React.Component { -

@@ -161,7 +160,7 @@ class Index extends React.Component { return (

Who is Using FDC3?

-

The Financial Desktop Connectivity and Collaboration Consortium (FDC3) standards are created and used by leading organizations across the financial industry. For more detail on who's using FDC3, developer tools, training and examples see the community page.

+

The Financial Desktop Connectivity and Collaboration Consortium (FDC3) standards are created and used by leading organizations across the financial industry.

{/* exclude button to users page for now, all users shown on main page */} {/*
diff --git a/website/pages/fdc3-roadmap.html b/website/pages/fdc3-roadmap.html index c9e05099d..5f539258a 100644 --- a/website/pages/fdc3-roadmap.html +++ b/website/pages/fdc3-roadmap.html @@ -200,18 +200,18 @@

FDC3 2.0 Roadmap

Issues Complete - ✅ - ✅ - ✅ - ✅ + soon + + + soon PRs Accepted By Working Group - ✅ 17th June + 26th May PRs Merged into Pre-Draft - ✅ 17th June + @@ -220,11 +220,11 @@

FDC3 2.0 Roadmap

Propose Pre-Draft - ✅ June 20th + Before June 23rd Vote - June 20th - June 30th By Consensus, + June 23rd - June 30th By Consensus, Determined By Maintainters diff --git a/website/siteConfig.js b/website/siteConfig.js index b8a11f428..bad3e1dbf 100644 --- a/website/siteConfig.js +++ b/website/siteConfig.js @@ -26,7 +26,7 @@ const siteConfig = { {doc: 'fdc3-intro', label: 'Getting Started'}, {page: 'fdc3-roadmap', label: 'Roadmap'}, {doc: 'use-cases/overview', label: 'Use Cases'}, - {page: 'community', label: 'Community'}, + //{page: 'implementations', label: 'Implementations'}, {doc: 'fdc3-standard', label: 'The Standard'}, {page: 'get-involved', label: 'Get Involved'}, {href: 'https://www.edx.org/course/fdc3-interoperability-for-the-financial-desktop', label: 'Training', external: true} diff --git a/website/static/css/custom.css b/website/static/css/custom.css index 3d8efbca0..ed2f3561d 100644 --- a/website/static/css/custom.css +++ b/website/static/css/custom.css @@ -287,18 +287,6 @@ article iframe { align-items: center; flex-basis: 100%; } -.implementation-details > .description > .badges { - padding-left: 10px; -} -.implementation-details > .description > .badges > .badge { - background-color: rgb(239, 239, 239); - display: inline-block; - border: 0px; - margin-top: 5px; - margin-right: 10px; -} - - @media screen and (max-width: 736px) { .implementation-details { padding: 6px 6px; diff --git a/website/static/img/community/groups.png b/website/static/img/community/groups.png deleted file mode 100644 index 66e31e859f8631d568cbeb5a1dd412fe81ac6c27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4810 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 bGr#J%K69nEgmG-tuxJajbEc)nZn6IWfZkv9 diff --git a/website/static/img/community/lf.png b/website/static/img/community/lf.png deleted file mode 100644 index 3922e1de7a2230243c366d2fdf4ef86d192e2c9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44415 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 diff --git a/website/static/img/users/FINOS.png b/website/static/img/users/FINOS.png deleted file mode 100644 index e9db360beda45cb06be79e95f145dff9dc53e62e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15414 zcmeHtZ6MS8`@fq?oet`#RPIWrv{R^rkU8C;6f3^DZLAV$ZYsB7sLoLmaxC|(VpN-1 z3^jLiDmRH8(}oEZ(abEdjji#2e>U~~{{H_5zbF3(zXv~0HrL+o&vm`8`}gbmB%N@w zSN&<@PYMbOsz;9;c2-bWLRL^v3R}JmP?T=5-4qn8q(=`Q_*WErV&E#)FY>_H0z&Ef zs&BBSlP^!wa9Z1M-(EhP_)JH%!S=HD(r|4R-{z^C+qZ|6T5;a}@%y$_2Y*?y`;S%2 zS1xNDArsDQ=e0O{W-)>t1Edp!A8C-_yW$-rIoOEP z4rrg%Wer#2`J134kr%KLmmDN`Y{V6ZysOz4wxVQIy?eqG7_Y{Kt>VI#rs;dGaEBi8 z<|(5voiV=)kJ*n8in-I}KvM!#lp0n@#0vdkyz5xudyQMJO6#D-k|P21qX8jD0yIk~ z){^aukJzt>x3hEK3pI%Sc3#9=o8qOe4ic1un%lEZE;jMSzr2!5*U_m{KYs!}F1o9*-B~biJDdxqL$JlC-LW=x6wCm37 zX&1JZ3;X=aP404itOQbKGcrpk+;u3*FTh5O2n_lx@3hr$ZGVLT*^ zH)#8vMbmG*qcC2UGy5nkXjuO{i~CsNTdc4j#=8OIt$ZjKeAj|QjGaSFxPz4yK1lP4 zT*5gwwuuWHS3&`LbGj@yLv}pg+5le<$2;CWDYF1=7gkt@71qFbdlcnM)+aK&?=l8+ z8KkuZ@&^G&c?ILOVk46O`Bi55pAQAtWH3@414K#3v|SXIIOVo%n!~RZ590 zp~Sq>{mw4pghR}E2P+u9Of%|-vRqu)Ate;`Vv6x?%O9t@ixsNCctxwCU_GZgG7A!2xVhm<8;D-OmDUjM0HGBTJ z>_9*^y<-78oY{s4@5l}WnZpIfi!Y_bywv>h>;o{KrVBg9g}toll}ubnV_2+^3*#xn zc+>jH^1Z+a92igJ!nSv2yPo;?N}$_ZY=pN%Kdj`3Xbsl*dLz7JF~wTx_K)4qVZ0D$ zHvW3j4_UzD%V0bc{eM${at}Myo?5?KrpxsXlxPnJD>M9nVdCN;H@oX#8QwXJ!90f9 z@=e=hN;`DR6>^0w_<~FZ+#ve81?7~c07oesbi)a0k#e3%g86i|LNV623q^D$8_ z$f}1-px$^yfCelm?fw~=ad!Q2K>(r!$XmMPXZhX-hY5N|0v3SAw|HjDcJ9Jn5Nv_* zZeb%TPn?m7LOb!0+3v!&a%K;F{3AzZs6)$LRZ1yt#S~EqMR5H)@s=yXDWEZ(;p7;A z)7<<DnYr|prc1$@e%v}EUx&;Ry^|7U^ZNqwK~3gGKYc2;iY^G{>JSMX#l zwpl3!WEno5QcB$pszO4-d9y#HXonbZDkfIwtEmqb0+ABQRWRNxRu~H7ZPL{HPV0AJ zpM>$Y8kqsYCRC2<`JPhB3upF9Bnl9q$G|BsmRuS}r5b25;GQ z;lkeG!frD98PFgx&WMVkgpu`p6(B$-!VY9J)DQtSA4gOG1+d&+yA5Bz6)#QD&;<1GMvBDl0uQq?=`$Irtn!*Z)oY{?`XMqGV%>*F=a6nQlqoJ=2;--|@x2oC#UjPcG zi-W|@U>Be*clSHg+l&=9V1+wiyf!CKKywdVvQul+)4|FJFC}KI0hD!CitUM&{mzgA z4f3-Ag_gC#-K??J3}0`Em)_4<4M?->B|G~UYIox6cjGZ>8qjf~e3ddgq{3oK8&)XB z3VBYJfcXCDYI0z3wF$le$MDVrYJ3Q(HVv0>UJ~r(%=UosPQiGq3^W0e9J1V9=w5x% z5%;vJsLZq9pP5TP&&=(vxY!Zm8_3HjH~|*X8Vpc_EaF27~874&w(r=RHD}(h5 zppN>wk}!W|EC;Kyy_7OBWhv!awuHwyUGcbZG%mxYd+ZrPWHI`2%d{{#mJM}!y@(|{ zC9f}-T6g=msuFj5JXg8SbtVlXn8Tj#&6J%Y~ zAtZ?D<#@ygZ^!^pEX?VS@ySG)obW@ zLMfimm_rgj*$no6S+c@Cd6Q=u$K>gmHD#{l{LoG$#;NfEeJU$WpUIf&O3jY0YZ~VU zp{e4oFoCF>>A&v-*q6}aMDxt{s`8tuqH$7+V!SXPJK?36YI?0PxAJWC42CRww*Sj;gVsRL#aA=)cfiJa z&iKEmVrrP6&*?B=_x{5QcNJkXOPG_p!P@%Lr(C%I(BPP~M`$5*DU!HbuPIB_K1>lu z*X~4O&QZmu4YopOV=f^on|nHT+?kgUy(HhdO)-O4kK)d26kVtaJi8r7iQZpphQxSL z#YzU-!TL=oo$Xpa=HiR*-yX-Uoa9b__z&9&wtE`l_N{^k2;3vl)M#I-_?(j~*yK)J zLc9jIAg2_r-&cP*%tR=+oJ4N9jTpmAZSJ^F87mFlG4%r(B;731yo!Ml*tm0>5(}sz z*Vvwrq>6!GMc_<=Cqn;Ti%F$knt4mxIt-L*Sa?BjsXv=kh$YQCQ^mg;>;W5>yPIWo zT|6snyGG~{S&Wh3r9=OW&9>V=@Z-#0us@#Us@`Z^Owc>-7H@^bJjvJywk+8h^YFje z3T#3SjSXZ8Ja#kvIoTHkvo+R8Yp_0?Dt2_b1h%XqPdwdkap{WRnlhmFxhyMjel!v{ z47dL2)UjrvPRe%{ma>~(pgaWUP@T+)16n83m*R>o=_zqu#nA@l8<&y93y8QliiJ*Ja2;{o4& z+qHe0SR3ZqF-|z9X;N1gxcvPlAbUDj=}Uf>V_Nn8wv>}*0&%gyQ`j9 zVxBq~B$%?2&-Z`r?s{#8C-|2T7Iq^sxyeu_TJA2{48^M{kQ*4LR|n%% zPoiJWNuDFJ%=MP5Fg_@TDxL}xq%oNO&+daw2?Z8<+*SjkYG_X{*S(quVAxa7UNWNB*xU(tY&=I6>4_&%Sq)ufqg<29qD! z1yqpmN|)?P;#5Wte2#O?&=TiIhTu#>CPP1-!lX7_0tp7hLivi@znR(8Zbl+AN%Q2D zkWqJLCRAN<1R2vFL<0sF%IAbOr(m3(RnSjrrWS5|=37Vq@bI2$aAXM~1mWg0es&4i zw9*h&cG~3jsctR%sqy?;Nq*cR+_148M&}X~JhxoUzLaHe#Owk8m1yst`$-eZBm3?u zv!0k1l`nM)(H`g@AC44S@WgO_L*NXBwpHDh06Y0Iv3^U5pUsASLX_+AkhWUm{&ZmZ z>8Dl;O|nKrjgnwmEo|?)2r{2uZ4v!(kHBKGyJ$JtH+F_|Z!M70+S3s}(?(P2W*T0l ze-e$&Xf=5dwt4ip7HKPBFugb318OlNL zV?Il%Vc)I+HyzEt^hpzxk%qgW{J#7oRV!!zMy1*-9f$Y}jgcooASFCl1|C?1GBq_3 z^DBdS(+y~G-SA)Vb_2WBsJ@q<=UxB4r5bL}>e4W>PY?GWjO)tGk78!^z zr@ANfE=_@l_+FR8`lr|I3C8v;ovUM8^MLS^bo-E68+=g5+c!6gZmJS7#IYn<-z>2P zW}p59>f-~bKF+;&{O%|Zp(0m*{qPR0udc__ChMzx14Gns?+VY^R9WyFCzCBCLPw7( zKXi$Ig#Sgqneg70_|D}JctG}1g1o}r)H>$IcvNpzEy&wb9T79$Y@PFNSMN_Tr2;a!<9@j*^p8R}b|MvC>fJN;?3n#()XcADU9Z*hFI0tnYYGcL2BdXy zYWy&tA1cZudN-b%S=%cpDu3C@tN$nyDe7-UbRZApIv0~7#Ql6s49Q^+x~=mzd1jvdK_8~6;2Whd)V9}BiGi@I`O)zZ^3 zZLl;%iwpd%6Zl&u@VAoK*-S}j6UNKLbozIhzcCVb$?h+@W~eeyB+9i0=og{(?LQq3 zV@t`M=$=0+Up?_RBe<;n>1*4|LuLSOzn$6A}HX8WL#ikaf zS9>nBnH5Z*X&!+5%X5^xh!^?vq<8zrT6j7p>6=aH>=yiEpd9J{YDNg$tOKMo$#ha} zwQuZz&=|8cf|;uflOs}JZc)Ww!vvs>8_-Vz+SIYAw{G!iuqYiOTa3M{+Dho*I}AKC z*WgNs`%#OW(sujRZ$}~vNrT%9d+T^#+PWNjvr=uci;UjoYLMRLn*_7-K|ab&Qmx%n z|CBzyR70+>D5c$8&>5JQ65K$pAM={WU#Qw)??W*(@UDXS57xoP25L<+R$9s>Bd48Z zH6q-G?vhwx;pOkoBBJbBP~Pgg*f)*@8K=yGGu|=RYM3Xtbfs0dmgG)3x3a=;M2?lw zv|kea@r3w7Qf8Ro!0u*U3?&!z=nv`r%|gGvmn4t}4d+Vfft?;T!A=4Fz*$fwe&Rc2 zUOSs);8TKhRb^p1slgKJn+Eh5y*;U}1j)_~{dq48B1d`=A#Jdp;Lg69kha&rpw^W! zYsy$gtVP~&cRUb|Zt+`?)`m!FepKbHnzK~;8QJ!XE!!eTo*kw&9;>Pl(b zHQB9yCoNm>Ff9BkHM(edn8GBmW`=TBD&NVi2J}MJBX{zuN8ZI$`$3_e0zyYqZcF!F zySa0m@jM2z{pEuP!BqO!Bq&K}SBtM{W4fwA5gE$rc} z&9wqqT{bm>?6xuc5_4Lr_lcVx+mx?B@OxCa98KHk7Y3 z;(F7&F`3{}uuZqs%BU|>gsj}^KR~rO-%)+MV`80L#P{eH;vojpt(2hW=vH&fQF3WV z2@sG<|LbYJ2mHAS=vvWE&;5>dHek@Mq`q`c&Oi=mY)2RM^5w!g7Lc*AkTf4k6_@M3 z)wttof1LN`GHx)_rM&;I_~+-)u4M!1korx#{qHRTw1~ZaI#^PJJ`#hVQeE@K(Onmx z&>2%eIT8M^-gee;89^W#4d-SHyi6K`{Y%Uz+eQ-Mg#8vLpaQEfy5~auUZk}pvfhRA z1e2PbpW-?kb7f>ERXcu;75hH14BQ8J9`(U)U}MKiF?@%uJc;x!P{n_Tg&(Cx4?Mi> zdK0v?_+j;u+DrC>p|7fAzciOwhzfZ*z2qI;S}}_#Z(<374&n<-@~-|;sJ&0CEc1@Q zBJf+2Hs(WWo#=Jh+&>*&p@Y`_CeJ@K9S@nPjK<*ZDW%J*?aw}Lja!fyB-Jv9G-$^>lRX1<8((}*Np(EJ8c#o!v$;Yi8dXl-$+ZFW$5cfARp z8W`gwnPA0!>kQi9Q?ny{c<^}e=St9_EtO&N_6%lLF~Po+5V`-sh47bI51ED97WF0| zxmxQzJA;N_?h%5T4D2EDJ9`LsZxKBI5*04^69h1WiPk#>Ab~40BQ*SQZ8!3niNWco zhI)0EW8hcm)yIkPyTZ@mk_6u|ca=;8-25hDK1dDsEldrk{p!i544)04_x zP_0RZiF)corsdOS`C{FIcz*JgCQRwbbqQ#g4uR_i{M^HPa%?nqgO@~SgO4cjPIHl-ZB326oHp1)R7`f{g(dJdxnHetZay&&;N{L^)X%WVi$rf|UL2?Ob&hiJ09Dcze z#?isb7|&a^@!u;W9AYpIdB+24UoBnavZ@RkEMOT%Yc&>^0Tlj>LyVV0t@rP{WI9J( zm>S2~eut2?$xkkPN0*gZ*^A!+ASnl-0pKX}1n?DSm7@QR*Qtba9Ge$hnNQ%5MUw$mb+m*FlN2#bn*LR24F#Zpc$TZf6@;O zc?%x12QR(s5L1!?p|tB(0(7_@j3vPACkDm~aDr}E?sou)JZM%z5#cF5jsb`(e*wv8 zX8}~ZE5gSi#?3*p;XB5+Gp}(V6B@1BOdf{pq+1A};K-h0N)4W(xo-|o6721170-vZ z%0@7u1(4Ec=fI;HhHk)u!W|^-SYeP81g*VK0@(6UpqGOr%0V)W6?Qm5%0vyVCQnT9 z=dlrX4w3?_aIb*|pzbmUs9Ip&(Ex)|iqFvi6{~B2nD86|llg^MVJB9om%j^;X!C&7 zGuQzOvUP}gngL0e?E}E%tH22HM*@cUF9Ag%@es{(qIMr%3d9LEfQA6Ojw`Xw_D^D~ zfv#*FB(4U~vR$!&*jtSiKE(=e!gvc)B@pVSwo{39a6r?U4Mu8UU|=3y1U*2-ND;j` zXdAvDn-K@Y>r8a00^-h}l~~srYGqTU_PQJhefje!&2m-bIR~#ohWCgnWTl;e%e4xE zGds_PExOtb`9yMZg6UhHV}L2Xels3ZYW^Fbk)u|)A7bBtl@(KL5CKEu9nj2Wl_FZ9 zKNp&3%x}W$d{l*KU*dlukDkN*aQ~@okQQd6mk^~@RWRNkSm88`hYHybhza(`Xt`YA z&5h$J(2EjZ0FTgmdTq^G@DEL6GfEcJ3OBa>@0b5o0&t1{Rl@%dCAhMSTNhuKa0?iF zlbG32O$Xymi8H&qz6)ZYTh3^J;ia|Mh)29>@P-L!O>{HHf_D$n%q?8l}-6{}xf=vOb zVLwuu(>MGL!x?XkBtiw^6`p`7NclD7h#SJmt+bn2%V6?27Z=tGUxf*p!vxo5_zWc7pJ zRNhJ$Of3ep&9(suiI~Vhn1%_;IO9CYdsAcvDbBZZe#%B{&-cf@N7tDmF~f>q;0x7Z z;o;QiK86!Rs<$K8hVNlwKCFWqyGqKSD&-RMqk3ww~5PW{GF=%zN= zH@nxZMFLLzCQQ({0t~Ea2j^Pe`j5@(kyIFy2)D-bdfMG-ycazbFdO`;Eu9P*xLH2k zqcRVVpNGelfBUK!-4Gu_jUEYnpG^<@;!z(+m3~oXQ6U zp)vk9_z0taxZFwx(`8x2oAT`Xw7#Z!1yG5Vz|^Bx$rVR(RYYI15&4u5wAAiS^K-)l z_f~#pi&@w7_>P=$fZ64J+YGAglAVg^-AjpZq_rJw<4d~4hrx`u`knp)jAU_^3FVK0 z%?7V3=fZEgeSNZ5)p1R?>WG$d3F#W!cH`+RjyQ>C?Prher})DZBew|e$aL;4GOT{LaH zo7ujUC>2&pKktVR1jh&IBdvXDJzd?J?|0j@=^gpcg~LmU77NekqgNN;yuf8y{maJa z*GlkFG=r%*cL=lkadRH3T-=@Wc!OY7iR3Y3D zIW3}Y7r&5*2KQ6}fA-vW*6I*zx)KYX|y@IqEWLUqs~7|5IplF7Ol}) z|7T#*=nr%X@O_Oq-)_@%l(9t9%YVF@AtEZqm)vJL=|Rt>r_?Yw5uZXfHUq(!03x zQB?8umbqN-d{RTW+4f07&-A(ZeLy9^YRGgF+y*HHuN`bv>Gd{~s5jjZHSJzY)*bLR zLRn96F<|s6ikrs-9&t;Hdbjx#!Dk_~n+ZT+e(p^Xvv%}zK~{OnQD&`1nVRC@VS@$fB)=WI?*!p0_45;!WXztSz{kE3LAH+ z*tGCFn|k)dN2R9DHG$s>qyzBGp1nLJHP9$0CYW*(esWK{Jht3@p$9E}B@LSZllLLA z9zA(suf%jfI#r3ZQYnw0gU6-BKgXp?^g83Hlj+`ckboqoX&}8S9s{A#ARN?3VVG#? zo49?Wn!rQ$sE;)Rt#KqAj9`H`+%rFgnPmqT+&s6?Q<*;g!MmG#y6*dCp~$U4?PUU! z*~nIJiGHQfnQqgEP(QyfOs`?IUGxEMb-plFvY9h}v!R(gSG>|(9u`WhEg{>G7}i5I zcO)j7N{vWOo5rbq-apr7@eC$jcwQvzLR()D76wVi!==*wdXg+W0qy70uE}7Agnj_M zIY=$XXwhL_&s&vQd84zl4Hpx}UaOZ7@)j0^lV;-;6-4FP^SjhGInaJP{Li&NPAVNe zoRMWp+T^HnqUqG{53}`m>^y9^y5ub@8@Y4U@uerRv46{~SA zsRmM;&=URua0gn2T0yp3g&NTDAdKyyz4i>ziQ63tWg->4E=(=3b6*dW%^Os8iOxs4S!#8fph+#iZp;y_A{0zp#v z6M0?4yHy_QoTxI4IvGAN(X;Gk=s+FygGKmzB{bKPMzyMNU7rcM1Cg}(cmflHSn9sg z2HrR2DbYfmw|;U`oqvmH|MLX<`Jyk2YGR~rWNRhQxRV*xP z^^CvYLqD3*i67fZCaLdzf_C)PL0tkTSWaHz{z8=;x89@wju|cYo03m*gU;^vTz@V{ z!v!fCnOVvTKRX;MMW=_KsO0K^yqvI=_6v&1OAmJ|(vl?GYom0S_kDT}vl!QW4wZ+z z%@V9T(H-i)hCR5(qx3LqG7$!{kixM9W>7-7vu{EFmy-G7^kwe;rk|N;`%9|4cc1r1 ze^a{14p?3I`!^@w0&Ne(6ban6<$MgkMaB6Bv1k3}`-*-dS7aVnzD<%)xJO7u(;H_- zZC4UG?a8y@Y`w!Qdwp(eE4%6bQs9v8+_?PG2ccRhX~)da{wE()zXfq)hX?5j=&WVd z)Ud-nU8_~a)748^VS#Cb>XG5T^(GZ+NyIW$Z-{SIgw2Y^qrQg8L|$}spq@v2UYw3U zT$#vR;16NqU*rjmQ?}DA$p_+{)^`PZ4&Jx%aYs2?31?N*jnar3`snhd^<40ZFiK5{ zxLK#xe1cP_;;v=Kx~D3d4C0bm@MZ3O_su0~fpE-I&>^|`4#ZzG#h?_XdCjes))m5J z;#T-)U`AlXH0veqDN4k)M(ML=No4PGC1U)*3oX9;47z{Xs#AEiP=)2 zCi)k6r+pK!=n8V2Fr=>XgKnGv9LFZS=Q2-cg`t~0Di z6&`?k3gB-_L}GzHH#ITl>WG3e(Qymfu>}(~uzalBRkgDqE6jc9cKVwaT+6FZR>|F9 zV_<^moR^C6_^~;OCMt;eo>Rcwl8?}`X9?<6$=ggPyY}5L_%x*sUjt=AAX1z%v2Doe z`G>eTiTBW0ccTLL0dUtA>)B**oUQ5Mj^)b4qXqqkS?2b8v>Jc$P#1n!6m5C~%5i?w zUu%-1qOt~MO+1hwcmnoF>{(Uy>g37CqK+*&X+vqmp(#bE{DZ?xZ$tr4(KCD3b4qyy zx%v*#%gS4Vw%>;{OCoA5(?B_1jC)6mfAGbir7XRwQvIm>NV$!W;zS{JH8JX&XwfHp z2YRQdL%aW;d;5`)d6WJ~sI1Q&zeaE4Zb2W@1MjUu5o8*#$QlXJ>%SDTU{ztpznV}L zccZ)+bK#F^&PmnIG-dy~MCleTLy`U(F?SzoWPqaQekN~=U(iq$iSoTw+%s~G_)#4` zS|4Jg{ceN~uSlk30z(+4SNSDOe_gK!@{^2Dxgu*_clq`z_At|0olI5x61T>q(V}8* zCQG$5dB}UjRO;z#ZCKHHi}Li5b>)Lrl4W?nZ1-%`=QtW-c>EvgE(`KyoeY!MXx$`=(cv|jZ+t7foqpOrYwza{?=86|wTD?lwyYZe zsKJGO6}o3zL18+iPVVeEp~zA~N4MoKbuTr2#B1O3EKzvzpDeyDOWEEsG=d*B`L6#d z<1PUB5_M6?`wx_eYU7reR3+jeEvASFvhKpW`v(5>ris4&6@$IA?`)c(wZQtI4vc(a z`rtAY(EWA-q$SAeJ$BYtyRZTB&kW}w#r#(eP`h{_O+E7 zG@{6I+1Mj_S?FWZviG!PDh2Jc(ZkJYf#ns$9@4PB1}ekP)`6?##Z*+KyLVgxUD0g3 zmnagpMxREMXW0Zio2=DG*ES1q)1mRx6|L-t{?_nL=xz$76;Q)0ful*N=_>9j=A=2W zE09jQH@4S%`f&<3{zd%~f0dNIG%@+#N{uO>d)T$iJwt#ryTNu7j#yp|7!I$-h=1eW zlB^N>2EDuCjq5+-gV;SHkDp@+;e68&9S^jfHi~CcBz80YYI=qJGb<|gg&0@2F8tj_ z4`cV2-XR-2YQlPZ8u$FzCLnMJBPoh3UyNQ(*G8jcVvv7q(1fl>PbVs<$Bw1EvFyW# zbis@8M;vr|)!EgfT}e8k2p>p|Q1~rz7nKQ_Thz$97QU`9bh$jBeN^F8D zsj8OGg5frmn+Os;n`b5!nH#>!6U9~cWLf;+{hF-xU%v15tQDIl6S;OE=dvQ3^vie^t)6Q=YDt3U zSyjGb(>I2w`0wv3rbooEi2TLiOTwDT&v|J@erW|5rXW!p*dkw)#a^i@xtH( zT+^dGjX37`;?YezmR(~pDFygIYqXuOj-H2`87bsX#UDWu?$y$64~biiQ1^DX!Ja!+ zWF2);oh7X2Xar>;8l$6L%`}GOo>9e_zWZ*`Dsol6g-l09f4DS$jq)!VwAoV;+%*Jx zLbd<0EO3p-koNfmQ;(rGC9F?$e*dGf{x`ELO5u7wroG=E$LxrXlUsV>*CTU(4u4GS zY57Si}I8K9}?5Vtuo8(5!-#GYyaLJ@Gv^xQe*ZJ@QpX zbx)@Kd-(!=Zc!+EEKeW3;}Gcd2FGmR`ZO7+!hxT&sEwlXI zIXQIWAW&9VR_Bud)&kp$_tyRvv?noE> zpLBUeS7MJVI^}YnQDnt~02pUOy{{ z|Gg!&>eJTYl9MTaI(X|(v$D4Y-P}sM)bF5H43&Le!jCpEsdCB z2UpLuV_oYm=SWejC77|9KW7npixm|THV>W--+)3sdaycO(4ZC*?N}M9*x{cbc;8BW za6>RZpFpRdPY?f>FLJM{so#c&-(r00_f%xkIf3pQJrGGmjV;{i?ozs^P(ls3A9uu8 zO8o$aK0nVTUKF)Pg9;yTt;Iwi?WURnt|qpuIGvfWp;pf>a;LQ=TjFoWdZ9wGY1bs% zfO?M6M|Y>^6Y`v5E+6zgW&dFik(yVEh%MNQQ3_c3;)AAm;eKoML0_E>9wDj3G+P!( z2+dVd@qIe!;nJCSyL{7u_AxtF>PBehbO*uutCf9uVj2Cc@>1b27c=ArX(-Ivpk*L|y+sn#-&4 diff --git a/website/static/img/users/GLUE42.png b/website/static/img/users/GLUE42.png index 9ba6813535059b1bbe632b69bba833653691575d..560c1ccc04e50377062435483a325c81ee597fe6 100644 GIT binary patch literal 11683 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 literal 7876 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 S \ No newline at end of file diff --git a/website/static/img/users/connectifi.png b/website/static/img/users/connectifi.png deleted file mode 100644 index 918256f24666be54774c245b9ea49cc084f275ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4735 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�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 R7Tkq`NQuddR*4t{{V%_}oc90# diff --git a/website/static/img/users/flextrade.jpg b/website/static/img/users/flextrade.jpg deleted file mode 100644 index c54ada4a10a939efd9e521915f5ba229451a1f5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 106468 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 diff --git a/website/static/img/users/pictet.png b/website/static/img/users/pictet.png deleted file mode 100644 index 9e55c66d062d89e88b01c3f7637544e4989897ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194891 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 z 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Π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%0Dm<*$QFklQJ>k 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 Wc8hl>nhv6~*|o#zm$K~vr~e1$wfgk{ diff --git a/website/static/img/users/spglobal.png b/website/static/img/users/spglobal.png deleted file mode 100644 index 564543c20c63e7da40fa2225a5ed57986366719e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47815 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 diff --git a/website/static/img/users/ubs.png b/website/static/img/users/ubs.png deleted file mode 100644 index be6b2251a9851b81524e23ebdb0626a922ff6f63..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 255513 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

    S_C+@Sk{Y)?a{M3ESRfX|D8CLTwF!b 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 Ka`CTDxBd@yPu7J1 diff --git a/website/static/js/implementationFilters.js b/website/static/js/implementationFilters.js index f0640e0e3..1161fc501 100644 --- a/website/static/js/implementationFilters.js +++ b/website/static/js/implementationFilters.js @@ -1,4 +1,4 @@ -const validTypes = ["platform-provider", "application-provider", "examples-and-training", "solution-provider", "adopter", "all"]; +const validTypes = ["platform-provider", "application-provider", "examples-and-training", "all"]; const setType = (theType) => { let typeToSet = theType; if (validTypes.indexOf(theType) === -1) { diff --git a/yarn.lock b/yarn.lock index 77a4954f1..82efb9544 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1802,16 +1802,11 @@ buffer-equal@0.0.1: resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-0.0.1.tgz#91bc74b11ea405bc916bc6aa908faafa5b4aac4b" integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs= -buffer-from@1.x: +buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - builtin-modules@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" @@ -4534,9 +4529,9 @@ mkdirp@0.x, mkdirp@^0.5.1: minimist "^1.2.5" moment@^2.22.1: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== mri@^1.1.0: version "1.1.6" @@ -5785,9 +5780,9 @@ source-map-resolve@^0.6.0: decode-uri-component "^0.2.0" source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -6142,9 +6137,9 @@ terminal-link@^2.0.0: supports-hyperlinks "^2.0.0" terser@^4.6.2: - version "4.8.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" - integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== dependencies: commander "^2.20.0" source-map "~0.6.1" From 5d300aa237b11dcf4f599b7cc9f0dade95e493d4 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Mon, 22 Aug 2022 09:27:42 +0200 Subject: [PATCH 38/61] typos --- docs/api-bridging/spec.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 0850a4036..c1629cdcb 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -175,7 +175,7 @@ A Desktop Agent can use the structure of this message to determine that it has c An optional JWT token can be included in the `hello` message to allow the connecting agent to authenticate the bridge. Verification of the supplied JWT by the DA is optional but recommended, meaning that the DA SHOULD verify the received JWT when one is included in the `hello` message. -If no hello message is received, the message doesn't match the defined format or validation of the optional JWT fails, the Desktop Agent should return to step 1 and attempt connection to the next port in the range. In the event that there are no ports reining in the range, the Desktop Agent SHOULD reset to the beginning of the range, MAY pause its attempts to connect and resume later. Note, if the Desktop Agent is configured to run at startup (of the user's machine) it is possible that the Desktop Agent Bridge may start later (or be restarted at some point). Hence, Desktop Agents SHOULD be capable of connecting to the bridge once they are already running (rather than purely at startup). +If no hello message is received, the message doesn't match the defined format or validation of the optional JWT fails, the Desktop Agent should return to step 1 and attempt connection to the next port in the range. In the event that there are no ports remaining in the range, the Desktop Agent SHOULD reset to the beginning of the range, MAY pause its attempts to connect and resume later. Note, if the Desktop Agent is configured to run at startup (of the user's machine) it is possible that the Desktop Agent Bridge may start later (or be restarted at some point). Hence, Desktop Agents SHOULD be capable of connecting to the bridge once they are already running (rather than purely at startup). ### Step 3. Handshake & Authentication @@ -205,8 +205,7 @@ The DA must then respond to the `hello` message with a `handshake` request to th } ``` -Note that the `meta` element of of the handshake message contains both a `timestamp` field (for logging purposes) and a `requestGuid` field that should be populated with a Globally Unique Identifier (GUID), generated by the Desktop Agent. This `responseGuid` will be used to link the handshake message to a response from the DAB that assigns it a name. For more details on GUID generation see section XX. - +Note that the `meta` element of of the handshake message contains both a `timestamp` field (for logging purposes) and a `requestGuid` field that should be populated with a Globally Unique Identifier (GUID), generated by the Desktop Agent. This `responseGuid` will be used to link the handshake message to a response from the DAB that assigns it a name. For more details on GUID generation see [Globally Unique Identifier](####globally-unique-identifier) section. If requested by the server, the JWT auth token payload should take the form: @@ -218,6 +217,7 @@ If requested by the server, the JWT auth token payload should take the form: ``` e.g. + ```JSON { "sub": "65141135-7200-47d3-9777-eb8786dd31c7", @@ -310,9 +310,9 @@ The `connectedAgentsUpdate` message will take the form: } ``` -When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast as the context being merged would become the most recent context on the channel, when other contexts may have been broadcast subsequently. Rather, it should be adopted internally by the Desktop Agent merging it such that it would be received by applications that are adding a user channel context listener or calling `channel.getCurrentContext()`. +When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast as the context being merged would become the most recent context on the channel, when other contexts may have been broadcast subsequently. Rather, it should be adopted internally by the Desktop Agent merging it such that it would be received by applications that are adding a user channel context listener or calling `channel.getCurrentContext()`. -It should be noted that Desktop Agents will not have context listeners for previously unknown channels, and SHOULD simply record that channel's state for use when that channel is first used. +It should be noted that Desktop Agents will not have context listeners for previously unknown channels, and SHOULD simply record that channel's state for use when that channel is first used. For known channel names, the Desktop Agent MUST also compare its current state to that which it has just received and may need to internally adopt context of types not previously seen on a channel. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as only the most recently transmitted Context should be received by un-typed listeners). Hence, the new context MUST only be passed to a context listener if it was registered specifically for that type and a context of that type did not previously exist on the channel. @@ -386,7 +386,7 @@ Request messages use the following format: /** AppIdentifier for the source application that the request was * received from and will be augmented with the assigned name of the * Desktop Agent by the Desktop Agent Bridge, rather than the sender. */ - source: AppIdentifier + source: AppIdentifier, /** Optional AppIdentifier for the destination that the request should be * routed to, which MUST be set by the Desktop Agent for API calls that * include a target (`app`) parameter. MUST include the name of the From d176d66804cca88bbc2aa14f992c12cbd0790809 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Mon, 22 Aug 2022 11:26:34 +0200 Subject: [PATCH 39/61] proposal update --- docs/api-bridging/spec.md | 61 ++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index c1629cdcb..791d39bf2 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -10,21 +10,10 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope ## Recent Changes -* Connection protocol updated - * Added initial connection step to connection protocol diagram - * Renumbered connection protocol steps to better map to diagram - * Improvements to connection protocol steps titles - * Added optional auth token to `hello` message from bridge to DA (allows 2-way authentication) - * Added description of atomicity requirements for handling of handshake and connectedAgentsUpdate messages by both DAB and DAs - * Channel State Synchronization - * Added details necessary to avoid race conditions (e.g. if a DA is connecting and another is broadcasting context concurrently) -* Recommended port range for bridge - (4470 - 4480) -* Interaction/Messaging protocol - * Added WIP draft - * Added section on GUID generation (with reference to external standard) used in requestGuid and responseGuid fields in messages. - * Added missing field comments for message formats -* Handling of slow responding DAs? - * Recommended timeout of 1500ms/3000ms +* Fixed typos found during last meeting +* Added new `DesktopAgentIdentifier` type +* Updated `meta.source` to be of `AppIdentifier` OR `DesktopAgentIdentifier` type +* Highlighted that `source.destination` field should be used for troubleshooting and debugging purposes only ## Open questions / TODO list @@ -34,6 +23,7 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Advise on whether other agents report to users on connect/disconnect events? * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction * Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to FDC3 references page +* Add a response collation example to generic messaging protocol ## Implementing a Desktop Agent Bridge @@ -383,15 +373,15 @@ Request messages use the following format: requestGuid: string, /** Timestamp at which request was generated */ timestamp: date, - /** AppIdentifier for the source application that the request was + /** AppIdentifier OR DesktopAgentIdentifier for the source application that the request was * received from and will be augmented with the assigned name of the * Desktop Agent by the Desktop Agent Bridge, rather than the sender. */ - source: AppIdentifier, - /** Optional AppIdentifier for the destination that the request should be + source: AppIdentifier | DesktopAgentIdentifier, + /** Optional AppIdentifier or DesktopAgentIdentifier for the destination that the request should be * routed to, which MUST be set by the Desktop Agent for API calls that * include a target (`app`) parameter. MUST include the name of the * Desktop Agent hosting the target application. */ - destination?: AppIdentifier + destination?: AppIdentifier | DesktopAgentIdentifier } } ``` @@ -426,9 +416,9 @@ Response messages will be differentiated from requests by the presence of a `res /** Timestamp at which request was generated */ timestamp: Date, /** AppIdentifier for the source that generated this response */ - source: AppIdentifier, - /** AppIdentifier for the destination that the response should be routed to */ - destination: AppIdentifier //TODO determine if this is actually needed as we can use the requestGuid... + source: AppIdentifier | DesktopAgentIdentifier, + /** AppIdentifier OR DesktopAgentIdentifier for the destination that the response should be routed to */ + destination: AppIdentifier | DesktopAgentIdentifier } } ``` @@ -439,28 +429,41 @@ Response messages MUST always include a `meta.destination` field which matches t Desktop Agents will prepare messages in the above format and transmit them to the bridge. However, to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge. This allows for multiple copies of the same underlying Desktop Agent implementation to be bridged and ensures that id clashes can be avoided. -To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation SHOULD retain a record of `requestGuid` fields, until teh request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. +To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppIdentifier` or `DesktopAgentIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation SHOULD retain a record of `requestGuid` fields, until teh request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of such augmentation is provided in the description of each message exchange. +#### DesktopAgentIdentifier + +This proposal introduces a new `DesktopAgentIdentifier` type. This type is to facilitate addressing of messages to particular Desktop Agent and apps that they host. + +```typescript +interface DesktopAgentIdentifier { + /** A string filled in by the Desktop Agent Bridge on receipt of a message, that represents + * the Desktop Agent Identifier that is the source of the message. + **/ + readonly desktopAgent?: string; +} +``` + #### AppIdentifier -To facilitate addressing of messages to particular Desktop Agents and apps that they host, `AppIdentifier` is expanded to contain a `desktopAgent` field. +The `AppIdentifier` is to be expanded to contain an optional `desktopAgent` field. ```typescript interface AppIdentifier { readonly appId: string; readonly instanceId?: string; - /** A string filled in by the bridge on receipt of a message, that represents - * the Desktop Agent that the app is available on. - **/ - readonly desktopAgent?: string; + /** Field that represents the Desktop Agent that the app is available on.**/ + readonly desktopAgent?: DesktopAgentIdentifier; } ``` ### Identifying Individual Messages -There are a variety of message types need to be sent between bridged DAs, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive an `IntentResolution` when an app has been chosen, and may subsequently receive an `IntentResult` after the intent handler has run). Hence, messages also need a unique identity, which should be generated at the Desktop Agent that is the source of that message, in the form of a Globally Unique Identifier (GUID). Response messages will include the identity of the request message they are related to, allowing multiple message exchanges to be 'in-flight' at the same time. +There are a variety of message types need to be sent between bridged Desktop Agents, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive an `IntentResolution` when an app has been chosen, and may subsequently receive an `IntentResult` after the intent handler has run). Hence, messages also need a unique identity, which should be generated at the Desktop Agent that is the source of that message, in the form of a Globally Unique Identifier (GUID). Response messages will include the identity of the request message they are related to, allowing multiple message exchanges to be 'in-flight' at the same time. + +The `meta.destination` field MUST not be relied upon to ensure the correct routing of messages. The spec recognizes the superior readability that this value provides over the `meta.requestGuid` but it should only be used for debugging and troubleshooting purposes. Hence, whenever a request message is generated by a Desktop Agent it should contain a unique `meta.requestGuid` value. Response messages should quote that same value in the `meta.requestGuid` field and generate a further unique identity for their response, which is included in the `meta.responseGuid` field. From 8ae9d96cf192f4d2f5578d16aef6bbba12988feb Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 23 Aug 2022 11:21:53 +0200 Subject: [PATCH 40/61] update --- docs/api-bridging/spec.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 791d39bf2..5433fa0a7 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -429,7 +429,7 @@ Response messages MUST always include a `meta.destination` field which matches t Desktop Agents will prepare messages in the above format and transmit them to the bridge. However, to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge. This allows for multiple copies of the same underlying Desktop Agent implementation to be bridged and ensures that id clashes can be avoided. -To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages should be added, by the bridge to `AppIdentifier` or `DesktopAgentIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation SHOULD retain a record of `requestGuid` fields, until teh request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. +To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages MUST be added, by the bridge to `AppIdentifier` or `DesktopAgentIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation MUST retain a record of `requestGuid` fields, until the request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of such augmentation is provided in the description of each message exchange. @@ -442,7 +442,7 @@ interface DesktopAgentIdentifier { /** A string filled in by the Desktop Agent Bridge on receipt of a message, that represents * the Desktop Agent Identifier that is the source of the message. **/ - readonly desktopAgent?: string; + readonly desktopAgent: string; } ``` From b69c9479b2da4872d11702f6c481e976a22230ca Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 23 Aug 2022 11:22:01 +0200 Subject: [PATCH 41/61] update --- docs/api-bridging/spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 5433fa0a7..2a1a243ba 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -429,7 +429,7 @@ Response messages MUST always include a `meta.destination` field which matches t Desktop Agents will prepare messages in the above format and transmit them to the bridge. However, to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge. This allows for multiple copies of the same underlying Desktop Agent implementation to be bridged and ensures that id clashes can be avoided. -To prevent spoofing and to simplify the implementation of clients, sender identities for bridging messages MUST be added, by the bridge to `AppIdentifier` or `DesktopAgentIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation MUST retain a record of `requestGuid` fields, until the request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. +To prevent spoofing and to simplify the implementation of clients, the source Desktop Agent identity for bridging messages MUST be added, by the bridge to `AppIdentifier` or `DesktopAgentIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation MUST retain a record of `requestGuid` fields, until the request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of such augmentation is provided in the description of each message exchange. From ba4bc238a00aba40717dfbc233f3d9ef0f80efca Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 23 Aug 2022 12:38:29 +0100 Subject: [PATCH 42/61] DAB proposal update: identifying messages, identifying agents, payload fields, response message routing, api calls that don't generate bridge requests, start on worklfows broken by disconnect --- docs/api-bridging/spec.md | 206 +++++++++++++++++++++++++------------- 1 file changed, 137 insertions(+), 69 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 2a1a243ba..9b64bd1ae 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -10,20 +10,24 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope ## Recent Changes -* Fixed typos found during last meeting -* Added new `DesktopAgentIdentifier` type -* Updated `meta.source` to be of `AppIdentifier` OR `DesktopAgentIdentifier` type -* Highlighted that `source.destination` field should be used for troubleshooting and debugging purposes only +* Fixed typos found during last meeting. +* Clarified use of DesktopAgent identity fields and update `meta.source` to be of `AppIdentifier` OR `DesktopAgentIdentifier` type. +* Added new `DesktopAgentIdentifier` type (for use on response messages that don't originate from an app, e.g., findIntent responses). +* Removed `meta.destination` from responses messages. Response collation and routing should be performed using `requestGuid` only. +* Completed set of payload fields used on request and response messages. + * with the exception of handling for PrivateChannel events +* Added a list of FDC3 API calls that do NOT generate bridge messages. ## Open questions / TODO list +* Expand on how the DAB should create the JWT token (and its claims, which must change to avoid replay attacks) which it sends out in the `hello` message for DAs to validate. * Add details of how to handle: * workflows broken by disconnection * An agent that is repeatedly timing out should be disconnected? - * Advise on whether other agents report to users on connect/disconnect events? + * Advise on whether other agents report to users on connect/disconnect events? (SHOULD) * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction * Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to FDC3 references page -* Add a response collation example to generic messaging protocol +* How to handle events from PrivateChannels (addContextListener, listener.unsubscribe, disconnect) ## Implementing a Desktop Agent Bridge @@ -361,11 +365,16 @@ Request messages use the following format: type: string, /** Request body, typically containing the arguments to the function called.*/ payload: { - //example fields for specific messages + /** Used to indicate which channel `broadcast` functions were called on. */ channel?: string, + /** Used as an argument to `findIntent` and `raiseIntent` functions.`*/ intent?: string, + /** Used as an argument to `broadcast`, `findIntent` and `raiseIntent` functions. */ context?: Context, - app?: AppIdentifier + /** Used as an argument to `open`, `raiseIntent`, `getAppMetadata`, and `findInstances` functions */ + app?: AppIdentifier, + /** Used as an argument to `findIntent` functions. */ + resultType?: string }, /** Metadata used to uniquely identify the message and its sender. */ meta: { @@ -388,11 +397,11 @@ Request messages use the following format: If the FDC3 API call underlying the request message includes a target (typically defined by an `app` argument, in the form of an AppIdentifier object) it is the responsibility of the Desktop Agent to copy that argument into the `meta.destination` field of the message and to ensure that it includes a `meta.destination.desktopAgent` value. If the target is provided in the FDC3 API call, but without a `meta.destination.desktopAgent` value, the Desktop Agent should assume that the call relates to a local application and does not need to send it to the bridge. -Requests without a `meta.destination` field will be forwarded to all other agents for processing and the collation of responses by the bridge. +Requests without a `meta.destination` field will be forwarded to all other agents by the bridge, which will also handle the collation of responses which quote the `meta.requestGuid`. #### Response Messages -Response messages will be differentiated from requests by the presence of a `responseGuid` field and MUST reference the `requestGuid` that they are responding to. +Response messages will be differentiated from requests by the presence of a `meta.responseGuid` field and MUST quote the `meta.requestGuid` that they are responding to. ```typescript { @@ -400,13 +409,23 @@ Response messages will be differentiated from requests by the presence of a `res type: string, /** Response body, containing the actual response data. */ payload: { - //example fields for specific messages... - intent?: string, + /** Standardized error strings from an appropriate FDC3 API Error enumeration. */ + error?: string, + /** Response to `open` */ + appIdentifier?: AppIdentifier, + /** Response to `findInstances` */ + appIdentifiers?: Array, + /** Response to `getAppMetadata` */ + appMetadata?: AppMetadata, + /** Response to `findIntent` functions*/ appIntent?: AppIntent, - - //TODO - - + /** Response to `raiseIntent` functions, returned on delivery of the intent and context to the target app. + * Note `getResult()` function should not / can not be included in JSON. */ + intentResolution?: IntentResolution, + /** Secondary response to `raiseIntent`, sent when the `IntentHandler` has returned. + * Note return an empty object if the `IntentHandler` returned void. + * Note `Channel` functions (`broadcast`, `getCurrentContext`, `addContextListener` should not / can not be included in JSON)*/ + intentResult?: {context?: Context, channel?: Channel}, }, meta: { /** requestGuid from the original request being responded to*/ @@ -417,38 +436,31 @@ Response messages will be differentiated from requests by the presence of a `res timestamp: Date, /** AppIdentifier for the source that generated this response */ source: AppIdentifier | DesktopAgentIdentifier, - /** AppIdentifier OR DesktopAgentIdentifier for the destination that the response should be routed to */ - destination: AppIdentifier | DesktopAgentIdentifier } } ``` -Response messages MUST always include a `meta.destination` field which matches the `meta.source` information provided in the request. Response messages that do not include a `meta.destination` should be discarded. +Response messages do not include a `meta.destination` as the routing of responses is handled by the bridge via the `meta.requestGuid` field. -### Identifying Desktop Agents Identity and Message Sources +### Identifying Individual Messages -Desktop Agents will prepare messages in the above format and transmit them to the bridge. However, to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge. This allows for multiple copies of the same underlying Desktop Agent implementation to be bridged and ensures that id clashes can be avoided. +There are a variety of message types need to be sent between bridged Desktop Agents, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive an `IntentResolution` when an app has been chosen, and may subsequently receive an `IntentResult` after the intent handler has run). Hence, messages also need a unique identity, which should be generated at the Desktop Agent that is the source of that message, in the form of a Globally Unique Identifier (GUID). Response messages will include the identity of the request message they are related to, allowing multiple message exchanges to be 'in-flight' at the same time. -To prevent spoofing and to simplify the implementation of clients, the source Desktop Agent identity for bridging messages MUST be added, by the bridge to `AppIdentifier` or `DesktopAgentIdentifier` objects embedded in them as the `source` field. Request and response `destination` fields are set by the Desktop Agent sending the message. However, in the case of response messages, Desktop Agent Bridge implementation MUST retain a record of `requestGuid` fields, until the request is fully resolved, allowing them to validate or overwrite the `destination` for a response to match the source of the original request, effectively enforcing the routing policy for interactions. +Hence, whenever a request message is generated by a Desktop Agent it should contain a unique `meta.requestGuid` value. Response messages should quote that same value in the `meta.requestGuid` field and generate a further unique identity for their response, which is included in the `meta.responseGuid` field. -Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of such augmentation is provided in the description of each message exchange. +Desktop Agent Bridge implementations should consider request messages that omit `meta.requestGuid` and response messages that omit either `meta.requestGuid` or `meta.responseGuid` to be invalid and should discard them. -#### DesktopAgentIdentifier +#### Globally Unique Identifier -This proposal introduces a new `DesktopAgentIdentifier` type. This type is to facilitate addressing of messages to particular Desktop Agent and apps that they host. +A GUID (globally unique identifier), also known as a Universally Unique IDentifier (UUID), is a generated 128-bit text string that is intended to be 'unique across space and time', as defined in [IETF RFC 4122](https://www.ietf.org/rfc/rfc4122.txt). -```typescript -interface DesktopAgentIdentifier { - /** A string filled in by the Desktop Agent Bridge on receipt of a message, that represents - * the Desktop Agent Identifier that is the source of the message. - **/ - readonly desktopAgent: string; -} -``` +There are several types of GUIDs, which vary how they are generated. As Desktop Agents will typically be running on the same machine, system clock and hardware details may not provide sufficient uniqueness in GUIDs generated (including during the connect step, where Desktop Agent name collisions may exist). Hence, it is recommended that both Desktop Agents and Desktop Agent Bridges SHOULD use a version 4 generation type (random). -#### AppIdentifier +### Identifying Desktop Agents Identity and Message Sources -The `AppIdentifier` is to be expanded to contain an optional `desktopAgent` field. +Desktop Agents will prepare messages in the above format and transmit them to the bridge. However, to target intents and perform other actions that require specific routing between DAs, DAs need to have an identity. Identities should be assigned to clients when they connect to the bridge. This allows for multiple copies of the same underlying Desktop Agent implementation to be bridged and ensures that id clashes can be avoided. + +To facilitate routing of messages between agents, the `AppIdentifier` is expanded to contain an optional `desktopAgent` field: ```typescript interface AppIdentifier { @@ -459,54 +471,110 @@ interface AppIdentifier { } ``` -### Identifying Individual Messages +Further, a new `DesktopAgentIdentifier` type is introduced to handle cases where a response message is returned by the Desktop Agent (or more specifically its resolver) rather than a specific app. This is particularly relevant for `findIntent` responses: -There are a variety of message types need to be sent between bridged Desktop Agents, several of which will need to be replied to specifically (e.g. a `fdc3.raiseIntent` call should receive an `IntentResolution` when an app has been chosen, and may subsequently receive an `IntentResult` after the intent handler has run). Hence, messages also need a unique identity, which should be generated at the Desktop Agent that is the source of that message, in the form of a Globally Unique Identifier (GUID). Response messages will include the identity of the request message they are related to, allowing multiple message exchanges to be 'in-flight' at the same time. - -The `meta.destination` field MUST not be relied upon to ensure the correct routing of messages. The spec recognizes the superior readability that this value provides over the `meta.requestGuid` but it should only be used for debugging and troubleshooting purposes. - -Hence, whenever a request message is generated by a Desktop Agent it should contain a unique `meta.requestGuid` value. Response messages should quote that same value in the `meta.requestGuid` field and generate a further unique identity for their response, which is included in the `meta.responseGuid` field. +```typescript +interface DesktopAgentIdentifier { + /** A string filled in by the Desktop Agent Bridge on receipt of a message, that represents + * the Desktop Agent Identifier that is the source of the message. + **/ + readonly desktopAgent: string; +} +``` -Desktop Agent Bridge implementations should consider request messages that omit `meta.requestGuid` and response messages that omit either `meta.requestGuid` or `meta.responseGuid` to be invalid and should discard them. +Hence, either an `AppIdentifier` or `DestkopAgentIdentifer` is used as the `meta.source` value of both request or respose messages and the source Desktop Agent identity for bridging messages will always be found at `meta.source.desktopAgent`. To prevent spoofing and to simplify the implementation of clients, the source Desktop Agent identity MUST be added to (or overwritten in) each message by the bridge when received. -#### Globally Unique Identifier +A request message may include a `destination` field, set by the source Desktop Agent if the message is intended for a particular Desktop Agent (e.g. to support a `raiseIntent` call with a specified target app or app instance on a particular Desktop Agent). -A GUID (globally unique identifier), also known as a Universally Unique IDentifier (UUID), is a generated 128-bit text string that is intended to be 'unique across space and time', as defined in [IETF RFC 4122](https://www.ietf.org/rfc/rfc4122.txt). +Response messages do not include a `destination` field. Instead, a Desktop Agent Bridge implementation MUST retain a record of `requestGuid` fields for request message, until the request is fully resolved, allowing them to determine the destination for the collated responses and effectively enforcing the routing policy for interactions. -There are several types of GUIDs, which vary how they are generated. As Desktop Agents will typically be running on the same machine, system clock and hardware details may not provide sufficient uniqueness in GUIDs generated (including during the connect step, where Desktop Agent name collisions may exist). Hence, it is recommended that both Desktop Agents and Desktop Agent Bridges SHOULD use a version 4 generation type (random). +Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of any such augmentation are provided in the description of each message exchange. ### Forwarding of Messages and Collating Responses When handling request messages, it is the responsibility of the Desktop Agent Bridge to: -* Receive request messages from connected Desktop Agents, -* Augment request messages with source information (as described above), +* Receive request messages from connected Desktop Agents. +* Augment request messages with `meta.source.desktopAgent` information (as described above). * Forward request messages onto either a specific Desktop Agent or all Desktop Agents as appropriate. For message exchanges that involve responses, it is the responsibility of the Desktop Agent Bridge to: -* Receive and collate response messages according the requestGuid (allowing multiple message exchanges to be 'in-flight' at once), -* Apply a timeout to the receipt of response messages for each request, -* Produce a single collated response message that incorporates the output of each individual response received, -* Deliver the collated response message to the source of the request - -There are a few simple rules which determine how a request message should be forwarded: - -* If the message is a request (`meta.requestGuid` is set, but `meta.responseGuid` is not) - * and the message does not include a `meta.destination` field - * forward it to all other Desktop Agents (not including the source) and await responses - * else if a `meta.destination` was included - * forward it to the specified destination agent and await the response -* else if the message is a response - * and the message response includes - -//TODO complete this - -## Handling FDC3 Interactions When Bridged - -//TODO add details of AppIdentifier augmentation to each exchange - -The use of Desktop Agent Bridging affects how a Desktop Agent must handle FDC3 API calls. Details on how this affects the FD3 API, how a Desktop Agent should interact the bridge and specifics of the messaging protocol are provided in this section. +* Receive and collate response messages according the `meta.requestGuid` (allowing multiple message exchanges to be 'in-flight' at once). +* Apply a timeout to the receipt of response messages for each request. +* Produce a single collated response message that incorporates the output of each individual response received. +* Deliver the collated response message to the source Desktop Agent that sent the request. + +Collated response messages generated by the bridge use the same format as individual response messages. + +The following pseudo-code defines how messages should be forwarded or collated: + +* if the message is a request (`meta.requestGuid` is set, but `meta.responseGuid` is not), + * and the message does not include a `meta.destination` field, + * forward it to all other Desktop Agents (not including the source), + * annotate the request as requiring responses from all connected agents, + * await responses or the specified timeout. + * else if a `meta.destination` was included, + * forward it to the specified destination agent + * annotate the request as requiring only a response from the specified agent, + * await the response or the specified timeout. +* else if the message is a response (both `meta.requestGuid` and `meta.responseGuid` are set) + * if the `meta.requestGuid` is known, + * add the message to the collated responses for the request, + * augment any `AppIdentifier` types in the reponse message with a `desktopAgent` field matching that of the responding Desktop Agent, + * if all expected responses have been received (i.e. all connected agents or he specified agent have responded, as appropriate), + * produce the collated response mesage and return to the requesting Desktop Agent. + * else await the configured response timeout or further responses, + * if the timeout is reached without any responses being received + * produce and return an appropriate [error response](../api/ref/Errors). + * else discard the response message (as it is a delayed to a request that has timed out or is otherwise invalid). +* else the message is invalid and should be discarded. + +//TODO add examples + +### Workflows Broken By Disconnects + +Targetted request and request/response workflows may be broken when a Desktop Agent disconnects from the bridge, which bridge implementations will need to handle. + +Three types of requests: +* Fire and forget +* Requests that require the bridge to collate multiple responses from the bridged Desktop Agents +* Requests targeted at a specific Desktop Agent + +The latter two types embody workflows that may be broken by an agent disconnecting from the bridge either before or during the processing of the request. + +Requests that will return error if disconnect occurs + +new ErrorEnumeration needed for bridge errors? + +* `findInstances(app: AppIdentifier)` - AppIdentifier w desktopAgent +findIntent - bridge will enforce timeout +findIntentByContext - bridge will enforce timeout +getAppMetadata - bridge will enforce timeout +open - error will be returned +raiseIntent - +raiseIntentForContext - + +Bridge disconnecting or crashing + +### FDC3 API calls that do NOT generate bridge messages + +Some FDC3 API calls can be handled locally and do not need to generate request messages to the Desktop Agent Bridge, but are likely to be involved in other exchanges that do generate messages to the bridge (for example adding context or intent handlers). Those calls include: + +* `addContextListener` functions +* `addIntentListener` +* `getOrCreateChannel` +* `createPrivateChannel` +* `getUserChannels` and `getSystemChannels` +* `joinUserChannel` and `joinChannel` +* `getCurrentChannel` +* `leaveCurrentChannel` +* `getInfo` + +//TODO deal with exceptions for PrivateChannel event handlers +// addContextListener +// listener.unsubscribe +// disconnect ## Individual message exchanges From 61bb38e22fcf7193957b3819e3f041a55c7a71b9 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Wed, 24 Aug 2022 10:43:57 +0200 Subject: [PATCH 43/61] update --- docs/api-bridging/spec.md | 55 +++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 9b64bd1ae..5b49b4b78 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -482,7 +482,7 @@ interface DesktopAgentIdentifier { } ``` -Hence, either an `AppIdentifier` or `DestkopAgentIdentifer` is used as the `meta.source` value of both request or respose messages and the source Desktop Agent identity for bridging messages will always be found at `meta.source.desktopAgent`. To prevent spoofing and to simplify the implementation of clients, the source Desktop Agent identity MUST be added to (or overwritten in) each message by the bridge when received. +Hence, either an `AppIdentifier` or `DesktopAgentIdentifier` is used as the `meta.source` value of both request or response messages and the source Desktop Agent identity for bridging messages will always be found at `meta.source.desktopAgent`. To prevent spoofing and to simplify the implementation of clients, the source Desktop Agent identity MUST be added to (or overwritten in) each message by the bridge when received. A request message may include a `destination` field, set by the source Desktop Agent if the message is intended for a particular Desktop Agent (e.g. to support a `raiseIntent` call with a specified target app or app instance on a particular Desktop Agent). @@ -521,9 +521,9 @@ The following pseudo-code defines how messages should be forwarded or collated: * else if the message is a response (both `meta.requestGuid` and `meta.responseGuid` are set) * if the `meta.requestGuid` is known, * add the message to the collated responses for the request, - * augment any `AppIdentifier` types in the reponse message with a `desktopAgent` field matching that of the responding Desktop Agent, + * augment any `AppIdentifier` types in the response message with a `desktopAgent` field matching that of the responding Desktop Agent, * if all expected responses have been received (i.e. all connected agents or he specified agent have responded, as appropriate), - * produce the collated response mesage and return to the requesting Desktop Agent. + * produce the collated response message and return to the requesting Desktop Agent. * else await the configured response timeout or further responses, * if the timeout is reached without any responses being received * produce and return an appropriate [error response](../api/ref/Errors). @@ -534,26 +534,53 @@ The following pseudo-code defines how messages should be forwarded or collated: ### Workflows Broken By Disconnects -Targetted request and request/response workflows may be broken when a Desktop Agent disconnects from the bridge, which bridge implementations will need to handle. +Targeted request and request/response workflows may be broken when a Desktop Agent disconnects from the bridge, which bridge implementations will need to handle. Three types of requests: + * Fire and forget * Requests that require the bridge to collate multiple responses from the bridged Desktop Agents -* Requests targeted at a specific Desktop Agent +* Requests targeted at a specific Desktop Agent and will need to be forwarded, by the bridge, to the target Desktop Agent The latter two types embody workflows that may be broken by an agent disconnecting from the bridge either before or during the processing of the request. -Requests that will return error if disconnect occurs +For requests that require the bridge to collate multiple responses the generic flow will be: + +* bridge will forward request to connected agents + * if agent disconnects + * bridge timeout will be reached + * bridge collates an error/timeout for disconnected agent??? + +If an Agent disconnects after the bridge sends the response to the requesting agent, and the requesting agent issues a targeted request to the disconnected Desktop Agent, it will go to the below scenario. + +For requests targeted at a specific Desktop Agent the generic flow will be: + +* bridge will check connected agents + * if target Desktop Agent found + * bridge forwards the request to target agent + * if Desktop Agent disconnects (bridge will update disconnected agents and ignore any response that may come) + * bridge timeout will be reached and error will be returned by the bridge + * otherwise bridge will send DesktopAgentNotFound error? + +//TODO new ErrorEnumeration needed for bridge errors? + +Requests that pass `AppIdentifier` as an argument - Assumed `AppIdentifier` is for an instance that was running in a Desktop Agent that disconnected + +`findInstances(app: AppIdentifier)` +`open(app: AppIdentifier, context?: Context)` +`getAppMetadata(app: AppIdentifier)` +`raiseIntent(intent: string, context: Context, app: AppIdentifier)` +`raiseIntentForContext(context: Context, app: AppIdentifier)` + +* Request issued with `AppIdentifier.desktopAgent` - Bridge will reply with AgentNotFound error +* Request issued without `AppIdentifier.desktopAgent` - Bridge will forward request to all connected agents - AppNotFound will be returned??? -new ErrorEnumeration needed for bridge errors? +Requests without `AppIdentifier` -* `findInstances(app: AppIdentifier)` - AppIdentifier w desktopAgent -findIntent - bridge will enforce timeout -findIntentByContext - bridge will enforce timeout -getAppMetadata - bridge will enforce timeout -open - error will be returned -raiseIntent - -raiseIntentForContext - +`findIntent(intent: string, context?: Context, resultType?: string)` +`findIntentsByContext(context: Context, resultType?: string)` +`raiseIntent(intent: string, context: Context)` +`raiseIntentForContext(context: Context)` Bridge disconnecting or crashing From faeba2141e503a7dd0ab17e5a2bbcf1e61f885f0 Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 24 Aug 2022 11:04:10 +0100 Subject: [PATCH 44/61] Bridge Workflows broken by disconnection --- docs/api-bridging/spec.md | 105 +++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 5b49b4b78..14361734d 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -17,17 +17,23 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Completed set of payload fields used on request and response messages. * with the exception of handling for PrivateChannel events * Added a list of FDC3 API calls that do NOT generate bridge messages. +* Procedure for forwarding of Messages and collation of responses +* Broke down previously specified bridge timeout range to be 1500ms on the bridge side and added Desktop Agent timeout (waiting for bridge responses) to be 3000ms + * Added recomendation on Desktop Agent timeout handling behavior when bridge stops responding + * Added behavior for an agent that is repeatedly timing out - Bridge should disconnect agent +* Added details of workflows broken by disconnection and agent and bridge behaviors + * Proposed new errors to add to Error enumerations +* Added recomendation of a minimum wait period of 5 seconds on reconnection attempts ## Open questions / TODO list * Expand on how the DAB should create the JWT token (and its claims, which must change to avoid replay attacks) which it sends out in the `hello` message for DAs to validate. -* Add details of how to handle: - * workflows broken by disconnection - * An agent that is repeatedly timing out should be disconnected? - * Advise on whether other agents report to users on connect/disconnect events? (SHOULD) -* Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction -* Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to FDC3 references page +* Advise on whether other agents report to users on connect/disconnect events? (SHOULD) * How to handle events from PrivateChannels (addContextListener, listener.unsubscribe, disconnect) +* To create final PR: + * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction + * Add new errors to Error enumerations specified in this proposal + * Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to FDC3 references page ## Implementing a Desktop Agent Bridge @@ -91,7 +97,7 @@ The DAB is the responsible entity for collating responses together from all DAs. The DAB MUST allow for timeout configuration. -The Bridge SHOULD also implement timeout for waiting on DA responses. Assuming the message exchange will be all intra-machine, a recommended timeout of 1500ms - 3000ms should be used. +The Bridge SHOULD also implement timeout for waiting on DA responses. Assuming the message exchange will be all intra-machine, a recommended timeout of 1500ms SHOULD be used. Similarly, Desktop Agents SHOULD apply a timeout to requests made to the bridge that require a response (collated or otherwise), to handle situations where the bridge is not responding as expected. A recommended timeout of 3000ms SHOULD be used in this case. #### Channels @@ -143,7 +149,11 @@ sequenceDiagram ### Step 1. Connect to Websocket -The Desktop Agent attempts to connect to the websocket at the first port in the defined port range. +The Desktop Agent attempts to connect to the websocket at the first port in the defined port range. If a connection cannot be made on the current port, move to the next port in the range and reattempt connection. + +In the event that there are no ports remaining in the range, the Desktop Agent SHOULD reset to the beginning of the range, SHOULD pause its attempts to connect and resume later (a minimum wait period of 5 seconds SHOULD be used) + +Note, if the Desktop Agent is configured to run at startup (of the user's machine) it is possible that the Desktop Agent Bridge may start later (or be restarted at some point). Hence, Desktop Agents SHOULD be capable of connecting to the bridge once they are already running (rather than purely at startup). ### Step 2. Hello @@ -169,7 +179,7 @@ A Desktop Agent can use the structure of this message to determine that it has c An optional JWT token can be included in the `hello` message to allow the connecting agent to authenticate the bridge. Verification of the supplied JWT by the DA is optional but recommended, meaning that the DA SHOULD verify the received JWT when one is included in the `hello` message. -If no hello message is received, the message doesn't match the defined format or validation of the optional JWT fails, the Desktop Agent should return to step 1 and attempt connection to the next port in the range. In the event that there are no ports remaining in the range, the Desktop Agent SHOULD reset to the beginning of the range, MAY pause its attempts to connect and resume later. Note, if the Desktop Agent is configured to run at startup (of the user's machine) it is possible that the Desktop Agent Bridge may start later (or be restarted at some point). Hence, Desktop Agents SHOULD be capable of connecting to the bridge once they are already running (rather than purely at startup). +If no hello message is received, the message doesn't match the defined format or validation of the optional JWT fails, the Desktop Agent should return to step 1 and attempt connection to the next port in the range. ### Step 3. Handshake & Authentication @@ -199,7 +209,7 @@ The DA must then respond to the `hello` message with a `handshake` request to th } ``` -Note that the `meta` element of of the handshake message contains both a `timestamp` field (for logging purposes) and a `requestGuid` field that should be populated with a Globally Unique Identifier (GUID), generated by the Desktop Agent. This `responseGuid` will be used to link the handshake message to a response from the DAB that assigns it a name. For more details on GUID generation see [Globally Unique Identifier](####globally-unique-identifier) section. +Note that the `meta` element of of the handshake message contains both a `timestamp` field (for logging purposes) and a `requestGuid` field that should be populated with a Globally Unique Identifier (GUID), generated by the Desktop Agent. This `responseGuid` will be used to link the handshake message to a response from the DAB that assigns it a name. For more details on GUID generation see [Globally Unique Identifier](#globally-unique-identifier) section. If requested by the server, the JWT auth token payload should take the form: @@ -496,7 +506,8 @@ When handling request messages, it is the responsibility of the Desktop Agent Br * Receive request messages from connected Desktop Agents. * Augment request messages with `meta.source.desktopAgent` information (as described above). -* Forward request messages onto either a specific Desktop Agent or all Desktop Agents as appropriate. +* Forward request messages onto either a specific Desktop Agent or all other Desktop Agents + * The bridge MUST NOT forward the request to the agent that sent the request, nor expect a reply from it. For message exchanges that involve responses, it is the responsibility of the Desktop Agent Bridge to: @@ -512,7 +523,7 @@ The following pseudo-code defines how messages should be forwarded or collated: * if the message is a request (`meta.requestGuid` is set, but `meta.responseGuid` is not), * and the message does not include a `meta.destination` field, * forward it to all other Desktop Agents (not including the source), - * annotate the request as requiring responses from all connected agents, + * annotate the request as requiring responses from all other connected agents, * await responses or the specified timeout. * else if a `meta.destination` was included, * forward it to the specified destination agent @@ -530,59 +541,57 @@ The following pseudo-code defines how messages should be forwarded or collated: * else discard the response message (as it is a delayed to a request that has timed out or is otherwise invalid). * else the message is invalid and should be discarded. -//TODO add examples - ### Workflows Broken By Disconnects Targeted request and request/response workflows may be broken when a Desktop Agent disconnects from the bridge, which bridge implementations will need to handle. Three types of requests: -* Fire and forget -* Requests that require the bridge to collate multiple responses from the bridged Desktop Agents -* Requests targeted at a specific Desktop Agent and will need to be forwarded, by the bridge, to the target Desktop Agent +1. Fire and forget (i.e. `broadcast`). +2. Requests that require the bridge to collate multiple responses from the bridged Desktop Agents (e.g. `findIntent`). +3. Requests targeted at a specific Desktop Agent that are forwarded to the target Desktop Agent (e.g. `raiseIntent`). The latter two types embody workflows that may be broken by an agent disconnecting from the bridge either before or during the processing of the request. -For requests that require the bridge to collate multiple responses the generic flow will be: - -* bridge will forward request to connected agents - * if agent disconnects - * bridge timeout will be reached - * bridge collates an error/timeout for disconnected agent??? - -If an Agent disconnects after the bridge sends the response to the requesting agent, and the requesting agent issues a targeted request to the disconnected Desktop Agent, it will go to the below scenario. +When processing the disconnection of agent from the bridge, the bridge MUST examine requests currently 'in-flight' and: -For requests targeted at a specific Desktop Agent the generic flow will be: +* For requests that require the bridge to collate multiple responses: + * complete those that no longer require further responses (all other agents have responded), or + * continue to await the timeout (if other agents are yet to respond), or + * return an 'empty' response in the expected format (if no other agents are connected and no data will be received). +* For requests that target a specific agent: + * return an appropriate error (as the request cannot be completed). -* bridge will check connected agents - * if target Desktop Agent found - * bridge forwards the request to target agent - * if Desktop Agent disconnects (bridge will update disconnected agents and ignore any response that may come) - * bridge timeout will be reached and error will be returned by the bridge - * otherwise bridge will send DesktopAgentNotFound error? +In the event that an Error must be returned (for requests that target a specific agent), it should be selected from the [Error enumeration](../api/ref/Errors) normally used by the corresponding FDC3 function (i.e. `OpenError` for `open` calls, `ResolveError` for `findIntent` and `raiseIntent` etc.). To facilitate easier debugging, errors specific to Desktop Agent Bridge are added to those enumerations, including: -//TODO new ErrorEnumeration needed for bridge errors? +```typescript +enum OpenError { + ... + /** Returned if the specified Desktop Agent is not found, via a connected + Desktop Agent Bridge. */ + DesktopAgentNotFound = 'DesktopAgentNotFound', +} -Requests that pass `AppIdentifier` as an argument - Assumed `AppIdentifier` is for an instance that was running in a Desktop Agent that disconnected +enum ResolveError { + ... + /** Returned if the specified Desktop Agent is not found, via a connected + Desktop Agent Bridge. */ + DesktopAgentNotFound = 'DesktopAgentNotFound', +} -`findInstances(app: AppIdentifier)` -`open(app: AppIdentifier, context?: Context)` -`getAppMetadata(app: AppIdentifier)` -`raiseIntent(intent: string, context: Context, app: AppIdentifier)` -`raiseIntentForContext(context: Context, app: AppIdentifier)` - -* Request issued with `AppIdentifier.desktopAgent` - Bridge will reply with AgentNotFound error -* Request issued without `AppIdentifier.desktopAgent` - Bridge will forward request to all connected agents - AppNotFound will be returned??? +enum ResultError { + ... + /** Returned if the specified Desktop Agent disconnected from the Desktop + Agent Bridge before a result was returned. */ + DesktopAgentDisconnected = 'DesktopAgentDisconnected', +} +``` -Requests without `AppIdentifier` +Finally, in the event that either a Desktop Agent or the bridge itself stops responding, but doesn't fully disconnect, the timeouts (specified earlier in this document) will be used to handle the request as if a disconnection had occurred. -`findIntent(intent: string, context?: Context, resultType?: string)` -`findIntentsByContext(context: Context, resultType?: string)` -`raiseIntent(intent: string, context: Context)` -`raiseIntentForContext(context: Context)` +In the event that a Desktop Agent repeatedly times out, the bridge SHOULD disconnect that agent (and update other agents via the `connectedAgentsUpdate` message specified in the connection protocol), to avoid all requests requiring the full timeout to complete. -Bridge disconnecting or crashing +In the event that the bridge repeatedly times out, connected Desktop Agents MAY disconnect from the bridge and attempt to reconnect by returning to Step 1 of the connection protocol. ### FDC3 API calls that do NOT generate bridge messages From 4dd3f6392e7e527c00abcfec8bb733a1ec9778bf Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Thu, 25 Aug 2022 08:55:02 +0200 Subject: [PATCH 45/61] typos spotted during meeting --- docs/api-bridging/spec.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 14361734d..be6cc3a66 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -17,7 +17,7 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Completed set of payload fields used on request and response messages. * with the exception of handling for PrivateChannel events * Added a list of FDC3 API calls that do NOT generate bridge messages. -* Procedure for forwarding of Messages and collation of responses +* Procedure for forwarding of Messages and collation of responses * Broke down previously specified bridge timeout range to be 1500ms on the bridge side and added Desktop Agent timeout (waiting for bridge responses) to be 3000ms * Added recomendation on Desktop Agent timeout handling behavior when bridge stops responding * Added behavior for an agent that is repeatedly timing out - Bridge should disconnect agent @@ -477,7 +477,7 @@ interface AppIdentifier { readonly appId: string; readonly instanceId?: string; /** Field that represents the Desktop Agent that the app is available on.**/ - readonly desktopAgent?: DesktopAgentIdentifier; + readonly desktopAgent?: string; } ``` @@ -533,12 +533,12 @@ The following pseudo-code defines how messages should be forwarded or collated: * if the `meta.requestGuid` is known, * add the message to the collated responses for the request, * augment any `AppIdentifier` types in the response message with a `desktopAgent` field matching that of the responding Desktop Agent, - * if all expected responses have been received (i.e. all connected agents or he specified agent have responded, as appropriate), + * if all expected responses have been received (i.e. all connected agents or the specified agent have responded, as appropriate), * produce the collated response message and return to the requesting Desktop Agent. * else await the configured response timeout or further responses, * if the timeout is reached without any responses being received * produce and return an appropriate [error response](../api/ref/Errors). - * else discard the response message (as it is a delayed to a request that has timed out or is otherwise invalid). + * else discard the response message (as it is a delayed response to a request that has timed out or is otherwise invalid). * else the message is invalid and should be discarded. ### Workflows Broken By Disconnects @@ -553,7 +553,7 @@ Three types of requests: The latter two types embody workflows that may be broken by an agent disconnecting from the bridge either before or during the processing of the request. -When processing the disconnection of agent from the bridge, the bridge MUST examine requests currently 'in-flight' and: +When processing the disconnection of an agent from the bridge, the bridge MUST examine requests currently 'in-flight' and: * For requests that require the bridge to collate multiple responses: * complete those that no longer require further responses (all other agents have responded), or From 5e0089ea72bec4bf1b5911807ef86de4c72f2121 Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 21 Sep 2022 12:08:50 +0100 Subject: [PATCH 46/61] Update to DAB spec --- docs/api-bridging/spec.md | 534 ++++++++++++++++++++++++++++---------- 1 file changed, 390 insertions(+), 144 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 14361734d..6b31e4e0f 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -10,20 +10,10 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope ## Recent Changes -* Fixed typos found during last meeting. -* Clarified use of DesktopAgent identity fields and update `meta.source` to be of `AppIdentifier` OR `DesktopAgentIdentifier` type. -* Added new `DesktopAgentIdentifier` type (for use on response messages that don't originate from an app, e.g., findIntent responses). -* Removed `meta.destination` from responses messages. Response collation and routing should be performed using `requestGuid` only. -* Completed set of payload fields used on request and response messages. - * with the exception of handling for PrivateChannel events -* Added a list of FDC3 API calls that do NOT generate bridge messages. -* Procedure for forwarding of Messages and collation of responses -* Broke down previously specified bridge timeout range to be 1500ms on the bridge side and added Desktop Agent timeout (waiting for bridge responses) to be 3000ms - * Added recomendation on Desktop Agent timeout handling behavior when bridge stops responding - * Added behavior for an agent that is repeatedly timing out - Bridge should disconnect agent -* Added details of workflows broken by disconnection and agent and bridge behaviors - * Proposed new errors to add to Error enumerations -* Added recomendation of a minimum wait period of 5 seconds on reconnection attempts +* source -> sources and timedOutSources in response messages to allow messaging to user when DAs don't respond +* adjust psuedocode for birdge message handling for cases where we receive a timeout but already have partial responses to process +* Exempted PrivateChannels from some cases where bridge messsages are not normally generated (but should be for PrivateChannels) + ## Open questions / TODO list @@ -91,7 +81,11 @@ Hence, message paths and propagation are simple. All messages to other Desktop A #### Collating Responses -Whilst some FDC3 requests are fire and forget (e.g. broadcast) the main requests such as `findIntent` or `raiseIntent` expect a response. In a bridging scenario, the response can come from multiple Desktop Agents and therefore need to be aggregated and augmented before they are sent back to the requesting DA. +Whilst some FDC3 requests are 'fire and forget' (e.g. broadcast) the main requests such as `findIntent` or `raiseIntent` expect a response. In a bridging scenario, the response can come from multiple Desktop Agents and therefore need to be aggregated and augmented before they are sent back to the requesting DA. + +:::note +A set of classifications for message exchange types are provided in the [Individual message exchanges](#individual-message-exchanges) section. +::: The DAB is the responsible entity for collating responses together from all DAs. Whilst this approach may add some complexity to bridge implementations, it will simplify DA implementations since they only need to handle one response. @@ -444,8 +438,13 @@ Response messages will be differentiated from requests by the presence of a `met responseGuid: string, /** Timestamp at which request was generated */ timestamp: Date, - /** AppIdentifier for the source that generated this response */ - source: AppIdentifier | DesktopAgentIdentifier, + /** Array of AppIdentifiers or DesktopAgentIdentifiers for the sources that generated + * responses to the request. Will contain a single value for individual responses and + * multiple values for responses that were collated by the bridge.*/ + sources: [AppIdentifier | DesktopAgentIdentifier], + /** Array of AppIdentifiers or DesktopAgentIdentifiers for responses that were not returned + * to the bridge before the timeout. May be omitted if all sources responded. */ + timedOutSources: [AppIdentifier | DesktopAgentIdentifier] } } ``` @@ -506,7 +505,7 @@ When handling request messages, it is the responsibility of the Desktop Agent Br * Receive request messages from connected Desktop Agents. * Augment request messages with `meta.source.desktopAgent` information (as described above). -* Forward request messages onto either a specific Desktop Agent or all other Desktop Agents +* Forward request messages onto either a specific Desktop Agent or all other Desktop Agents. * The bridge MUST NOT forward the request to the agent that sent the request, nor expect a reply from it. For message exchanges that involve responses, it is the responsibility of the Desktop Agent Bridge to: @@ -518,7 +517,7 @@ For message exchanges that involve responses, it is the responsibility of the De Collated response messages generated by the bridge use the same format as individual response messages. -The following pseudo-code defines how messages should be forwarded or collated: +The following pseudo-code defines how messages should be forwarded or collated by the bridge: * if the message is a request (`meta.requestGuid` is set, but `meta.responseGuid` is not), * and the message does not include a `meta.destination` field, @@ -533,14 +532,20 @@ The following pseudo-code defines how messages should be forwarded or collated: * if the `meta.requestGuid` is known, * add the message to the collated responses for the request, * augment any `AppIdentifier` types in the response message with a `desktopAgent` field matching that of the responding Desktop Agent, - * if all expected responses have been received (i.e. all connected agents or he specified agent have responded, as appropriate), + * if all expected responses have been received (i.e. all connected agents or the specified agent has responded, as appropriate), * produce the collated response message and return to the requesting Desktop Agent. * else await the configured response timeout or further responses, * if the timeout is reached without any responses being received - * produce and return an appropriate [error response](../api/ref/Errors). + * produce and return an appropriate [error response](../api/ref/Errors), including details of all Desktop Agents in `timedOutSources`. + * log the timeout for each Desktop Agent that did not respond and check disconnection criteria. + * if the timeout is reached with a partial set of responses + * produce and return a collated response, but include details of Desktop Agents that timed out in `timedOutResponses`. + * log the timeout for each Desktop Agent that did not respond and check disconnection criteria. * else discard the response message (as it is a delayed to a request that has timed out or is otherwise invalid). * else the message is invalid and should be discarded. +//TODO add detail on DA responsibilities and how requests should be handled by the desktop agent (with timeouts and messaging to user on timedOutSources) + ### Workflows Broken By Disconnects Targeted request and request/response workflows may be broken when a Desktop Agent disconnects from the bridge, which bridge implementations will need to handle. @@ -597,7 +602,8 @@ In the event that the bridge repeatedly times out, connected Desktop Agents MAY Some FDC3 API calls can be handled locally and do not need to generate request messages to the Desktop Agent Bridge, but are likely to be involved in other exchanges that do generate messages to the bridge (for example adding context or intent handlers). Those calls include: -* `addContextListener` functions +* `addContextListener` functions (excluding those for `PrivateChannel` instances) +* `listener.unsubscribe` (excluding those for `PrivateChannel` instances) * `addIntentListener` * `getOrCreateChannel` * `createPrivateChannel` @@ -607,10 +613,11 @@ Some FDC3 API calls can be handled locally and do not need to generate request m * `leaveCurrentChannel` * `getInfo` -//TODO deal with exceptions for PrivateChannel event handlers -// addContextListener -// listener.unsubscribe -// disconnect +However, `PrivateChannel` instances allow the registration of additional event handlers (for the addition or removal of context listeners) that may be used to manage streaming data sent over them by starting or stopping the stream in response to those events. Hence, the following calls DO generate request messages when used on a PrivateChannel instance: + +* `addContextListener` +* `listener.unsubscribe` +* `disconnect` ## Individual message exchanges @@ -623,14 +630,22 @@ Each section assumes that we have 3 agents connected by a bridge: * agent-C * DAB -## Context +Message exchanges come in a number of formats, which are known as: -### broadcast (on channels) +* **Request only**: A request message that does not require a response ('fire and forget'), such as a `broadcast`. +* **Request Response (single)**: A request message that expects a single response from a single Desktop Agent, such as `open` or `getAppMetadata`. +* **Request Response (collated)**: A request message that expects responses from all other Desktop Agents that are collated by the bridge and returned as a single response to the requestor, such as `findIntent` or `findInstances`. +* **Request Multiple Response (single)**: A request message that expects multiple responses from a single Desktop Agent, such as `raiseIntent`. -Only needs a single message (no response). +### `broadcast` (on `fdc3`, a `Channel` or `PrivateChannel`) -An app on agent-A does: +Type: **Request only** +Generated by API calls: +* [`fdc3.broadcast(contextObj)`](../api/ref/DesktopAgent#broadcast) +* [`Channel.broadcast(contextObj)`](../api/ref/Channel#broadcast) + +e.g. ```javascript fdc3.broadcast(contextObj); ``` @@ -641,15 +656,17 @@ or (await fdc3.getOrCreateChannel("myChannel")).broadcast(contextObj) ``` +Message exchange: + ```mermaid sequenceDiagram participant DA as Desktop Agent A participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C - DA ->>+ DAB: Broadcast - DAB ->>+ DB: Broadcast - DAB ->>+ DC: Broadcast + DA ->>+ DAB: broadcast + DAB ->>+ DB: broadcast + DAB ->>+ DC: broadcast ``` It encodes this as a message which it sends to the DAB @@ -664,12 +681,9 @@ It encodes this as a message which it sends to the DAB }, "meta": { "requestGuid": "requestGuid", - "timestamp": "2020-03-...", + "timestamp": "2022-03-...", "source": { - "name": "...", - "appId": "...", - "version": "...", - // ... other metadata fields + "appId": "..." } } } @@ -691,25 +705,33 @@ which it repeats on to agent-B AND agent-C with the `source.desktopAgent` metada "timestamp": "2020-03-...", "source": { "desktopAgent": "agent-A", - "name": "...", - "appId": "...", - "version": "...", - // ... other metadata fields - }, + "appId": "..." + } } } ``` -When adding context listeners (either for User Channels or specific App Channels) no messages need to be exchanged. Instead, upon receiving a broadcast message the Desktop Agent just needs to pass it on to all listeners on that named channel. +### findIntent -## Intents +Type: **Request Response (collated)** -### findIntent +Generated by API calls: +* [`findIntent(intent, context)`](../api/ref/DesktopAgent#findintent) +* [`findIntentsByContext(context)`](../api/ref/DesktopAgent#findintentsbycontext) -```typescript -findIntent(intent: string, context?: Context): Promise; +e.g. +```javascript +let appIntent = await fdc3.findIntent("StartChat", context); ``` +or + +```javascript +let appIntentArr = await fdc3.findIntentsByContext(context); +``` + +Message exchange: + ```mermaid sequenceDiagram participant DA as Desktop Agent A @@ -724,15 +746,7 @@ sequenceDiagram DAB -->>- DA: findIntentResponse (B + C) ``` -#### Request format - -A findIntent call is made on agent-A. - -```javascript -let appIntent = await fdc3.findIntent(); -``` - -Sends an outward message to the DAB. +Outward message to the DAB. ```JSON // agent-A -> DAB @@ -746,10 +760,7 @@ Sends an outward message to the DAB. "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { - "name": "", - "appId": "", - "version": "", - // ... other metadata fields + "appId": "" } } } @@ -771,10 +782,7 @@ The DAB fills in the `source.desktopAgent` field and forwards the request to the "timestamp": "2020-03-...", "source": { "desktopAgent": "agent-A", // filled by DAB - "name": "", - "appId": "", - "version": "", - // ... other metadata fields + "appId": "" } } } @@ -795,16 +803,18 @@ Normal response from agent-A, where the request was raised. } ``` -DA agent-B would produce response: +DA agent-B produces response: ```JSON { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "Skype" }, - { "name": "Symphony" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859" }, - { "name": "Slack" } + { "appId": "Skype", "title": "Skype" /* other AppMetadata fields may be included */}, + { "appId": "Symphony", "title": "Symphony" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "Symphony" }, + { "appId": "Slack", "title": "Slack" } ] } ``` @@ -820,10 +830,12 @@ which is sent back over the bridge as a response to the request message as: "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "Skype"}, - { "name": "Symphony" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859" }, - { "name": "Slack" } + { "appId": "Skype", "title": "Skype" /* other AppMetadata fields may be included */ }, + { "appId": "Symphony", "title": "Symphony" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "Symphony" }, + { "appId": "Slack", "title": "Slack" } ] } }, @@ -833,10 +845,7 @@ which is sent back over the bridge as a response to the request message as: "timestamp": "2020-03-...", "destination": { "desktopAgent": "agent-A", - "name": "", - "appId": "", - "version": "", - // ... other metadata fields + "appId": "" } } } @@ -854,10 +863,13 @@ This response gets repeated by the bridge in augmented form as: "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "Skype", "desktopAgent": "agent-B"}, - { "name": "Symphony", "desktopAgent": "agent-B" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, - { "name": "Slack", "desktopAgent": "agent-B" } + { "appId": "Skype", "desktopAgent": "agent-B", "title": "Skype"}, + { "appId": "Symphony", "desktopAgent": "agent-B", "title": "Symphony" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "desktopAgent": "agent-B", + "title": "Symphony" }, + { "appId": "Slack", "desktopAgent": "agent-B" } ] } }, @@ -867,9 +879,7 @@ This response gets repeated by the bridge in augmented form as: "timestamp": "2020-03-...", "destination": { "desktopAgent": "agent-A", - "name": "", "appId": "", - "version": "", // ... other metadata fields }, "source": { @@ -901,7 +911,7 @@ which is sent back over the bridge as a response to the request message as: "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "WebIce", "desktopAgent": "agent-C"} + { "appId": "WebIce", "desktopAgent": "agent-C"} ] } }, @@ -911,10 +921,7 @@ which is sent back over the bridge as a response to the request message as: "timestamp": "2020-03-...", "destination": { "desktopAgent": "agent-A", - "name": "", - "appId": "", - "version": "", - // ... other metadata fields + "appId": "" } } } @@ -930,7 +937,7 @@ This response gets repeated by the bridge in augmented form as: "appIntent": { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "WebIce", "desktopAgent": "agent-C"} + { "appId": "WebIce", "desktopAgent": "agent-C"} ] } }, @@ -940,10 +947,7 @@ This response gets repeated by the bridge in augmented form as: "timestamp": "2020-03-...", "destination": { "desktopAgent": "agent-A", - "name": "", - "appId": "", - "version": "", - // ... other metadata fields + "appId": "" }, "source": { "desktopAgent": "agent-C", @@ -959,12 +963,12 @@ Then on agent-A the originating app finally gets back the following response fro { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "name": "myChat" }, // local to this agent - { "name": "Skype", "desktopAgent": "agent-B" }, //agent-B responses - { "name": "Symphony", "desktopAgent": "agent-B" }, - { "name": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, - { "name": "Slack", "desktopAgent": "agent-B" }, - { "name": "WebIce", "desktopAgent": "agent-C"} //agent C response + { "appId": "myChat" }, // local to this agent + { "appId": "Skype", "desktopAgent": "agent-B" }, //agent-B responses + { "appId": "Symphony", "desktopAgent": "agent-B" }, + { "appId": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, + { "appId": "Slack", "desktopAgent": "agent-B" }, + { "appId": "WebIce", "desktopAgent": "agent-C"} //agent C response ] } ``` @@ -1017,10 +1021,7 @@ agent-A sends an outward `findIntent` message to the DAB: "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { - "name": "someOtherApp", //should this be the Desktop Agent or the app? - "appId": "...", - "version": "...", - // ... other metadata fields + "appId": "..." //should this be the Desktop Agent or the app? } } } @@ -1054,14 +1055,10 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise DAB -> agent-A @@ -1239,19 +1220,13 @@ If intent result is private channel: "timestamp": "2020-03-...", "error?:": "ResultError Enum", "source": { - "name": "AChatApp", "appId": "", - "version": "", "desktopAgent": "agent-B" // filled by DAB - // ... other metadata fields }, "destination": { // duplicates the app argument "app": { - "name": "someOtherApp", "appId": "", - "version": "", - "desktopAgent": "agent-A" - // ... other metadata fields + "desktopAgent": "agent-A" } } } @@ -1587,8 +1562,279 @@ sequenceDiagram DB -->>- DA: Return App Data ``` -## Channels -App Channels don't need specific messages sending for `fdc3.getOrCreateChannel` as other agents will be come aware of it when messages are broadcast. +### PrivateChannels + +`PrivateChannels` provide some additional event handlers for the addition or removal of context listeners and are intended to provide a private communication channel for applications. Hence, there is a difference in how their broadcasts SHOULD be handled and a number of additional message exchanges necessary for their events. + +#### `broadcast` + +Desktop Agents SHOULD provide special handling for `PrivateChannel` broadcasts. A copy of the broadcast message SHOULD be forwarded to the bridge for each `ContextListener` added by a remote Desktop Agent, with a `destination` field set. Doing so will require the Desktop Agent (in addition to the app that owns the `PrivateChannel`) to track which applications have added a `ContextListener` to the `PrivateChannel`. + +Hence, the broadcast message should be modified to: + +```JSON +// DAB -> agent-B +{ + "type": "broadcast", + "payload": { + "channel": "private-channel-ABC123", + "context": { /*contextObj*/} + }, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "desktopAgent": "agent-A", + "appId": "..." + }, + "destination": { + + } + } +} +``` + +//TODO intent result is a private channel + +```JSON +// agent-B -> DAB -> agent-A +{ + "type": "intentResult", + "payload?:": { + "channel": { + "id": "channel a", + "type": "private" + }, + "context": {/*contextObj*/} // in alternative to channel + }, + "meta": { + "requestGuid": "requestGuid", + "responseGuid": "intentResultResponseGuid", + "timestamp": "2020-03-...", + "error?:": "ResultError Enum", + "source": { + "appId": "AChatApp", + "desktopAgent": "agent-B" // filled by DAB + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-A" + // ... other metadata fields + } + } + } +} +``` + + + +#### `addContextListener` + +```JSON +// DAB -> agent-B +{ + "type": "PrivateChannel.onAddContextListener", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + "desktopAgent": "agent-A" + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + +The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) + +```JSON +// agent-A -> DAB +{ + "type": "PrivateChannel.onAddContextListener", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + +#### `listener.unsubscribe` + +```JSON +// agent-A -> DAB +{ + "type": "PrivateChannel.onUnsubscribe", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp" + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + +The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) + +```JSON +// DAB -> agent-B +{ + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "type": "privateChannelUnsubscribe", + "payload": {}, + "meta": { + "source": { + "appId": "AChatApp", + "desktopAgent": "agent-A", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + + +#### `onDisconnect` + +```JSON +// agent-A -> DAB +{ + "type": "PrivateChannel.onDisconnect", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + +The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) + +```JSON +// DAB -> agent-B +{ + "type": "PrivateChannel.onDisconnect", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + "desktopAgent": "agent-A" + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + + + +--- +`onDisconnect` to the private channel sent to the bridge -However, `PrivateChannel` instances do require additional handling due to the listeners for subscription and disconnect. Please see the raiseIntent section for the messages sent in support of this functionality. +```JSON +// agent-A -> DAB +{ + "type": "privateChannelDisconnect", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` + +The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) + +```JSON +// DAB -> agent-B +{ + "type": "privateChannelDisconnect", + "payload": {}, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + "desktopAgent": "agent-A" + // ... other metadata fields + }, + "destination": { // duplicates the app argument + "app": { + "appId": "someOtherApp", + "desktopAgent": "agent-B" + // ... other metadata fields + } + } + } +} +``` \ No newline at end of file From 17115aa7bd83f553c2d82eb128ded71590a558b2 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Thu, 22 Sep 2022 10:41:25 +0100 Subject: [PATCH 47/61] typos cleanup linting errors --- docs/api-bridging/spec.md | 91 ++++++++------------------------------- 1 file changed, 18 insertions(+), 73 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 4e70e31c0..114c583c7 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -11,9 +11,8 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope ## Recent Changes * source -> sources and timedOutSources in response messages to allow messaging to user when DAs don't respond -* adjust psuedocode for birdge message handling for cases where we receive a timeout but already have partial responses to process -* Exempted PrivateChannels from some cases where bridge messsages are not normally generated (but should be for PrivateChannels) - +* adjust pseudocode for bridge message handling for cases where we receive a timeout but already have partial responses to process +* Exempted PrivateChannels from some cases where bridge messages are not normally generated (but should be for PrivateChannels) ## Open questions / TODO list @@ -143,7 +142,7 @@ sequenceDiagram ### Step 1. Connect to Websocket -The Desktop Agent attempts to connect to the websocket at the first port in the defined port range. If a connection cannot be made on the current port, move to the next port in the range and reattempt connection. +The Desktop Agent attempts to connect to the websocket at the first port in the defined port range. If a connection cannot be made on the current port, move to the next port in the range and reattempt connection. In the event that there are no ports remaining in the range, the Desktop Agent SHOULD reset to the beginning of the range, SHOULD pause its attempts to connect and resume later (a minimum wait period of 5 seconds SHOULD be used) @@ -642,10 +641,12 @@ Message exchanges come in a number of formats, which are known as: Type: **Request only** Generated by API calls: + * [`fdc3.broadcast(contextObj)`](../api/ref/DesktopAgent#broadcast) * [`Channel.broadcast(contextObj)`](../api/ref/Channel#broadcast) e.g. + ```javascript fdc3.broadcast(contextObj); ``` @@ -716,10 +717,12 @@ which it repeats on to agent-B AND agent-C with the `source.desktopAgent` metada Type: **Request Response (collated)** Generated by API calls: + * [`findIntent(intent, context)`](../api/ref/DesktopAgent#findintent) * [`findIntentsByContext(context)`](../api/ref/DesktopAgent#findintentsbycontext) e.g. + ```javascript let appIntent = await fdc3.findIntent("StartChat", context); ``` @@ -1198,7 +1201,7 @@ When `AChatApp` produces a response, or the intent handler finishes running, it ``` If intent result is a channel: - +// TODO - Fill this If intent result is private channel: * see [PrivateChannels](#privatechannels) @@ -1562,7 +1565,6 @@ sequenceDiagram DB -->>- DA: Return App Data ``` - ### PrivateChannels `PrivateChannels` provide some additional event handlers for the addition or removal of context listeners and are intended to provide a private communication channel for applications. Hence, there is a difference in how their broadcasts SHOULD be handled and a number of additional message exchanges necessary for their events. @@ -1629,12 +1631,10 @@ Hence, the broadcast message should be modified to: } ``` - - -#### `addContextListener` +#### `onAddContextListener` ```JSON -// DAB -> agent-B +// agent-A -> DAB { "type": "PrivateChannel.onAddContextListener", "payload": {}, @@ -1643,7 +1643,6 @@ Hence, the broadcast message should be modified to: "timestamp": "2020-03-...", "source": { "appId": "AChatApp", - "desktopAgent": "agent-A" // ... other metadata fields }, "destination": { // duplicates the app argument @@ -1660,7 +1659,7 @@ Hence, the broadcast message should be modified to: The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) ```JSON -// agent-A -> DAB +// DAB -> agent-B { "type": "PrivateChannel.onAddContextListener", "payload": {}, @@ -1669,6 +1668,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "timestamp": "2020-03-...", "source": { "appId": "AChatApp", + "desktopAgent": "agent-A" // ... other metadata fields }, "destination": { // duplicates the app argument @@ -1698,7 +1698,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des }, "destination": { // duplicates the app argument "app": { - "appId": "someOtherApp", + "appId": "someOtherApp", "desktopAgent": "agent-B" // ... other metadata fields } @@ -1712,11 +1712,11 @@ The bridge will add in the source agent (agent-A) and forward the message to des ```JSON // DAB -> agent-B { - "requestGuid": "requestGuid", - "timestamp": "2020-03-...", - "type": "privateChannelUnsubscribe", + "type": "PrivateChannel.onUnsubscribe", "payload": {}, "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", "source": { "appId": "AChatApp", "desktopAgent": "agent-A", @@ -1733,7 +1733,6 @@ The bridge will add in the source agent (agent-A) and forward the message to des } ``` - #### `onDisconnect` ```JSON @@ -1750,10 +1749,10 @@ The bridge will add in the source agent (agent-A) and forward the message to des }, "destination": { // duplicates the app argument "app": { - "appId": "someOtherApp", + "appId": "someOtherApp", "desktopAgent": "agent-B" // ... other metadata fields - } + } } } } @@ -1784,57 +1783,3 @@ The bridge will add in the source agent (agent-A) and forward the message to des } } ``` - - - ---- -`onDisconnect` to the private channel sent to the bridge - -```JSON -// agent-A -> DAB -{ - "type": "privateChannelDisconnect", - "payload": {}, - "meta": { - "requestGuid": "requestGuid", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) - -```JSON -// DAB -> agent-B -{ - "type": "privateChannelDisconnect", - "payload": {}, - "meta": { - "requestGuid": "requestGuid", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - "desktopAgent": "agent-A" - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` \ No newline at end of file From c7324ab9121cdf4c0245d1210d5de6c51f6602fe Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 27 Sep 2022 12:53:41 +0100 Subject: [PATCH 48/61] adding message exchange content --- docs/api-bridging/spec.md | 648 ++++++++++++++++++++++++-------------- 1 file changed, 407 insertions(+), 241 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 4e70e31c0..603f392a9 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -15,8 +15,15 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Exempted PrivateChannels from some cases where bridge messsages are not normally generated (but should be for PrivateChannels) -## Open questions / TODO list +## Open questions +* Confirm handling of error responses + * Desktop Agent API should always return JavaScript errors. Doe we want to pass the entire stack trace or just the error message? (It should be possible to find the stack trace in the Desktop Agent in which the error occurred) +* On a `findIntent` call, should the requesting Desktop Agent send its own findIntentResult to the bridge and receive its own response back (collated with other findIntent responses), or should it receive the responses from the bridge and augment its own response without sending it to the bridge? + +## TODO list + +* Complete message exchange documentation * Expand on how the DAB should create the JWT token (and its claims, which must change to avoid replay attacks) which it sends out in the `hello` message for DAs to validate. * Advise on whether other agents report to users on connect/disconnect events? (SHOULD) * How to handle events from PrivateChannels (addContextListener, listener.unsubscribe, disconnect) @@ -421,8 +428,10 @@ Response messages will be differentiated from requests by the presence of a `met appIdentifiers?: Array, /** Response to `getAppMetadata` */ appMetadata?: AppMetadata, - /** Response to `findIntent` functions*/ + /** Response to `findIntent`*/ appIntent?: AppIntent, + /** Response to `findIntentsByContext`*/ + appIntents?: AppIntent[], /** Response to `raiseIntent` functions, returned on delivery of the intent and context to the target app. * Note `getResult()` function should not / can not be included in JSON. */ intentResolution?: IntentResolution, @@ -711,25 +720,18 @@ which it repeats on to agent-B AND agent-C with the `source.desktopAgent` metada } ``` -### findIntent +### `findIntent` (on `fdc3`) Type: **Request Response (collated)** -Generated by API calls: +Generated by API call: * [`findIntent(intent, context)`](../api/ref/DesktopAgent#findintent) -* [`findIntentsByContext(context)`](../api/ref/DesktopAgent#findintentsbycontext) -e.g. +e.g. An application with appId `agentA-app1` makes the following API call: ```javascript let appIntent = await fdc3.findIntent("StartChat", context); ``` -or - -```javascript -let appIntentArr = await fdc3.findIntentsByContext(context); -``` - Message exchange: ```mermaid @@ -746,27 +748,28 @@ sequenceDiagram DAB -->>- DA: findIntentResponse (B + C) ``` -Outward message to the DAB. +#### Request format +Outward message to the DAB: ```JSON // agent-A -> DAB { - "type": "findIntent", - "payload": { + "type": "findIntent", + "payload": { "intent": "StartChat", "context": {/*contextObj*/} - }, - "meta": { + }, + "meta": { "requestGuid": "requestGuid", "timestamp": "2020-03-...", "source": { - "appId": "" + "appId": "agentA-app1" } - } + } } ``` -The DAB fills in the `source.desktopAgent` field and forwards the request to the other Desktop Agents (agent-B AND agent-C). +The DAB fills in the `source.desktopAgent` field and forwards the request to the other Desktop Agents (agent-B AND agent-C): ```JSON // DAB -> agent-B @@ -775,14 +778,14 @@ The DAB fills in the `source.desktopAgent` field and forwards the request to the "type": "findIntent", "payload": { "intent": "StartChat", - "context": {/*contextObj*/}, + "context": {/*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "desktopAgent": "agent-A", // filled by DAB - "appId": "" + "appId": "agentA-app1" } } } @@ -796,18 +799,18 @@ Normal response from agent-A, where the request was raised. ```JSON { - "intent": { "name": "StartChat", "displayName": "Chat" }, + "intent": { "name": "StartChat" }, "apps": [ - { "name": "myChat" } + { "appId": "myChat" } ] } ``` -DA agent-B produces response: +DA agent-B would produce the following response if the request was generated locally: ```JSON { - "intent": { "name": "StartChat", "displayName": "Chat" }, + "intent": { "name": "StartChat" }, "apps": [ { "appId": "Skype", "title": "Skype" /* other AppMetadata fields may be included */}, { "appId": "Symphony", "title": "Symphony" }, @@ -819,16 +822,15 @@ DA agent-B produces response: } ``` -which is sent back over the bridge as a response to the request message as: +Hence, the response it sends to the bridge is encoded as follows: ```JSON // agent-B -> DAB { "type": "findIntentResponse", "payload": { - "intent": "StartChat", "appIntent": { - "intent": { "name": "StartChat", "displayName": "Chat" }, + "intent": { "name": "StartChat" }, "apps": [ { "appId": "Skype", "title": "Skype" /* other AppMetadata fields may be included */ }, { "appId": "Symphony", "title": "Symphony" }, @@ -840,62 +842,22 @@ which is sent back over the bridge as a response to the request message as: } }, "meta": { - "requestGuid": "requestGuid", - "responseGuid": "responseGuidAgentB", + "requestGuid": "", + "responseGuid": "", "timestamp": "2020-03-...", - "destination": { - "desktopAgent": "agent-A", - "appId": "" - } } } ``` -Note the response guid generated by the agent-B and the reference to the request guid produced by agent-A where the request was originated. - -This response gets repeated by the bridge in augmented form as: - -```JSON -{ - "type": "findIntentResponse", - "payload": { - "intent": "StartChat", - "appIntent": { - "intent": { "name": "StartChat", "displayName": "Chat" }, - "apps": [ - { "appId": "Skype", "desktopAgent": "agent-B", "title": "Skype"}, - { "appId": "Symphony", "desktopAgent": "agent-B", "title": "Symphony" }, - { "appId": "Symphony", - "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", - "desktopAgent": "agent-B", - "title": "Symphony" }, - { "appId": "Slack", "desktopAgent": "agent-B" } - ] - } - }, - "meta": { - "requestGuid": "requestGuid", - "responseGuid": "responseGuidAgentB", - "timestamp": "2020-03-...", - "destination": { - "desktopAgent": "agent-A", - "appId": "", - // ... other metadata fields - }, - "source": { - "desktopAgent": "agent-B", - } - } -} -``` +Note the response GUID generated by the agent-B and the reference to the request GUID produced by agent-A where the request was originated. Further, note that the `AppMetadata` elements in the `AppIntent` do not have a `desktopAgent` field yet, which the bridge will add. -DA agent-C would produce response: +DA agent-C would produce the following response locally: ```JSON { - "intent": { "name": "StartChat", "displayName": "Chat" }, + "intent": { "name": "StartChat" }, "apps": [ - { "name": "WebIce"} + { "appId": "WebIce"} ] } ``` @@ -907,138 +869,293 @@ which is sent back over the bridge as a response to the request message as: { "type": "findIntentResponse", "payload": { - "intent": "StartChat", "appIntent": { - "intent": { "name": "StartChat", "displayName": "Chat" }, + "intent": { "appId": "StartChat" }, "apps": [ - { "appId": "WebIce", "desktopAgent": "agent-C"} + { "appId": "WebIce"} ] } }, "meta": { - "requestGuid": "requestGuid", - "responseGuid": "responseGuidAgentC", + "requestGuid": "", + "responseGuid": "", "timestamp": "2020-03-...", - "destination": { - "desktopAgent": "agent-A", - "appId": "" - } } } ``` -This response gets repeated by the bridge in augmented form as: +The bridge receives and collates the responses, producing the following collated response which is sends back to agent-A: ```JSON +// DAB -> agent-A { "type": "findIntentResponse", "payload": { "intent": "StartChat", "appIntent": { - "intent": { "name": "StartChat", "displayName": "Chat" }, + "intent": { "name": "StartChat" }, "apps": [ + { "appId": "Skype", "title": "Skype", "desktopAgent": "agent-B" }, + { "appId": "Symphony", "title": "Symphony", "desktopAgent": "agent-B" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "Symphony", + "desktopAgent": "agent-B" }, + { "appId": "Slack", "title": "Slack", "desktopAgent": "agent-B" }, { "appId": "WebIce", "desktopAgent": "agent-C"} ] } }, "meta": { - "requestGuid": "requestGuid", - "responseGuid": "responseGuidAgentC", + "requestGuid": "", + "responseGuid": "", "timestamp": "2020-03-...", - "destination": { - "desktopAgent": "agent-A", - "appId": "" - }, - "source": { - "desktopAgent": "agent-C", - } } } ``` -Then on agent-A the originating app finally gets back the following response from the bridge: +Finally, agent-A combines the data received from the bridge, with its own local response to produce the response to the requesting application: ```JSON // DAB -> agent-A { "intent": { "name": "StartChat", "displayName": "Chat" }, "apps": [ - { "appId": "myChat" }, // local to this agent - { "appId": "Skype", "desktopAgent": "agent-B" }, //agent-B responses - { "appId": "Symphony", "desktopAgent": "agent-B" }, - { "appId": "Symphony", "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", "desktopAgent": "agent-B" }, - { "appId": "Slack", "desktopAgent": "agent-B" }, - { "appId": "WebIce", "desktopAgent": "agent-C"} //agent C response + // local to this agent + { "appId": "myChat" }, + //agent-B responses + { "appId": "Skype", "title": "Skype", "desktopAgent": "agent-B" }, + { "appId": "Symphony", "title": "Symphony", "desktopAgent": "agent-B" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "Symphony", + "desktopAgent": "agent-B" }, + { "appId": "Slack", "title": "Slack", "desktopAgent": "agent-B" }, + //agent-C responses + { "appId": "WebIce", "desktopAgent": "agent-C"} ] } ``` -### raiseIntent - -```typescript -raiseIntent(intent: string, context: Context, app?: TargetApp): Promise; -``` - -For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app:TargetApp` argument. If one is not passed a `findIntent` will be sent instead to collect options to display in a local resolver UI, allowing for a targeted intent to be raised afterwards. See details below. +### `findIntentsByContext` (on `fdc3`) -When receiving a response from invoking `raiseIntent` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppIdentifier` with an `instanceId`. +Type: **Request Response (collated)** -Note that the below diagram assumes a `raiseIntent` WITH a `app:TargetApp` was specified and therefore agent-C is not involved. +Generated by API call: +* [`findIntentsByContext(context)`](../api/ref/DesktopAgent#findintentsbycontext) -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DAB as Desktop Agent Bridge - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DAB: raiseIntent - DAB ->>+ DB: raiseIntent - DB -->>- DAB: intentResolution - DAB -->>- DA: intentResolution - DB ->>+ DAB: intentResult - DAB ->>+ DA: intentResult +e.g. An application with appId `agentA-app1` makes the following API call: +```javascript +let appIntentArr = await fdc3.findIntentsByContext(context); ``` +:::note +The message exchanges for this API call are nearly identical to that used for `findIntent()`, differing only by the lack of an `intent` field in the request message payload and the structure of the response message (where an array of `AppIntents` is returned). +::: + #### Request format -A raiseIntent call, __without__ `app:TargetApp` argument is made on agent-A. +Outward message to the DAB: -```typescript -raiseIntent(intent: string, context: Context): Promise; +```JSON +// agent-A -> DAB +{ + "type": "findIntentsForContext", + "payload": { + "context": {/*contextObj*/} + }, + "meta": { + "requestGuid": "requestGuid", + "timestamp": "2020-03-...", + "source": { + "appId": "agentA-app1" + } + } +} ``` -agent-A sends an outward `findIntent` message to the DAB: +The DAB fills in the `source.desktopAgent` field and forwards the request to the other Desktop Agents (agent-B AND agent-C). ```JSON -// agent-A -> DAB +// DAB -> agent-B +// DAB -> agent-C { - "type": "findIntent", + "type": "findIntentsForContext", "payload": { - "intent": "StartChat", "context": {/*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { - "appId": "..." //should this be the Desktop Agent or the app? + "desktopAgent": "agent-A", // filled by DAB + "appId": "agentA-app1" } } } ``` -This will trigger the same flow as `findIntent`. Upon receiving a `findIntentResponse`, the resolver is shown. +#### Response format -User selects an option which will trigger a `raiseIntent` call with a `app:TargetApp` argument. +An individual agent (for example agentB) would generate a local response as an array of `AppIntent` objects: ---- +```JSON +[ + { + "intent": { "name": "StartChat" }, + "apps": [ + { "appId": "Skype", "title": "Skype" /* other AppMetadata fields may be included */}, + { "appId": "Symphony", "title": "Symphony" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "Symphony" }, + { "appId": "Slack", "title": "Slack" } + ] + }, + { + "intent": { "name": "ViewProfile" }, + "apps": [ + { "appId": "myCRM", "title": "My CRM" }, + { "appId": "myCRM", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "My CRM" } + ] + } +] +``` -A `raiseIntent` call is made on agent-A which targets an `AChatApp` in agent-B. +This response is encoded and sent to the bridge as: -```typescript -raiseIntent(intent: string, context: Context, app: TargetApp): Promise; +```JSON +// agent-B -> DAB +{ + "type": "findIntentsForContextResponse", + "payload": { + "appIntents": [ + { + "intent": { "name": "StartChat" }, + "apps": [ + { "appId": "Skype", "title": "Skype" /* other AppMetadata fields may be included */}, + { "appId": "Symphony", "title": "Symphony" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "Symphony" }, + { "appId": "Slack", "title": "Slack" } + ] + }, + { + "intent": { "name": "ViewProfile" }, + "apps": [ + { "appId": "myCRM", "title": "My CRM" }, + { "appId": "myCRM", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "My CRM" } + ] + } + ] + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-...", + } +} +``` + +Each `AppMetadata` object is augmented by the bridge with a `desktopAgent` field and collated with responses from other agents into a response to the requesting agent: + +```JSON +// DAB -> agent-A +{ + "type": "findIntentsForContextResponse", + "payload": { + "appIntents": [ + { + "intent": { "name": "StartChat" }, + "apps": [ + //agent-B responses + { "appId": "Skype", "title": "Skype", "desktopAgent": "agent-B" }, + { "appId": "Symphony", "title": "Symphony", "desktopAgent": "agent-B" }, + { "appId": "Symphony", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "Symphony", + "desktopAgent": "agent-B" }, + { "appId": "Slack", "title": "Slack", "desktopAgent": "agent-B" }, + //agent-C response + { "appId": "WebIce", "desktopAgent": "agent-C"} + ] + }, + { + "intent": { "name": "ViewProfile" }, + "apps": [ + //agent-A responses + { "appId": "myCRM", "title": "My CRM", "desktopAgent": "agent-B" }, + { "appId": "myCRM", + "instanceId": "93d2fe3e-a66c-41e1-b80b-246b87120859", + "title": "My CRM", + "desktopAgent": "agent-B" } + //agent-C responses + { "appId": "riskToolkit", "title": "Client Risk Toolkit", "desktopAgent": "agent-c" }, + { "appId": "linkedIn", "title": "LinkedIn", "desktopAgent": "agent-c" } + ] + } + ] + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-...", + } +} +``` + +Finally agent-A combines the payload received with it own response and returns it to the requesting application. + +### raiseIntent + +Type: **Request Multiple Response (single)** + +Generated by API call: +* [`raiseIntent(intent, context, app)`](../api/ref/DesktopAgent#raiseIntent) + +For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app: TargetApp` argument. If one is not passed, then the `findIntent` message exchange should be used to collect options for the local resolver to use. Once an option has been selected (for example because there is only one option, or because the user selected an option in a local intent resolver UI), the `raiseIntent` message exchange may then be used (if a remote option was selected as the resolution) to raise the intent. + +e.g. An application with appId `agentA-app1` makes the following API call: + +```javascript +let appIntent = await fdc3.raiseIntent("StartChat", context); +``` + +Agent A should then conduct the `findIntent` message exchange as described above, displaying its Intent resolver UI if necessary. Once an option is selected, the `raiseIntent` message exchange is conducted as if the API call had been made with a target app: + +```javascript +let appIntent = await fdc3.raiseIntent("StartChat", context, {"appId": "Slack", "desktopAgent": "agent-B"}); +``` + +Message exchange: + +:::note +Agent-C is not involved in the disagram below as the `raiseIntent` is now specified withtarget applicaiton and Desktop Agent. +::: + +```mermaid +sequenceDiagram + participant DA as Desktop Agent A + participant DAB as Desktop Agent Bridge + participant DB as Desktop Agent B + participant DC as Desktop Agent C + DA ->>+ DAB: raiseIntent + DAB ->>+ DB: raiseIntent + DB -->>- DAB: intentResolution + DAB -->>- DA: intentResolution + DB ->>+ DAB: intentResult + DAB ->>+ DA: intentResult ``` +#### Request format + +Outward message to the DAB: + ```JSON // agent-A -> DAB { @@ -1046,19 +1163,20 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise", "timestamp": "2020-03-...", "source": { - "appId": "..." + "appId": "agentA-app1" }, "destination": { // duplicates the app argument so that the message is routed like any other "app": { + "appId": "Slack", "desktopAgent": "agent-B" } } @@ -1066,27 +1184,31 @@ raiseIntent(intent: string, context: Context, app: TargetApp): Promise agent-B { "type": "raiseIntent", "payload": { - "intent": "StartChat", + "intent": "StartChat", "context": {/*contextObj*/}, + "app": { + "appId": "Slack", + "desktopAgent": "agent-B" + //an instanceId may be included to target an already running instance + } }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { - "appId": "...", - "desktopAgent": "agent-A" // <---- filled by DAB - // ... other metadata fields + "appId": "agentA-app1", + "desktopAgent": "agent-A" // filled in by DAB }, "destination": { "app": { - "name": "AChatApp", + "appId": "Slack", "desktopAgent": "agent-B" } }, @@ -1096,143 +1218,159 @@ The bridge fills in the `source.desktopAgent` field and forwards the request to #### Response format -Normal response from agent-B, where the request was targeted to by agent-A. It sends this `intentResolution` as soon as it delivers the `raiseIntent` to the target app +If the `raiseIntent` request were made locally, agent-B would deliver the intent and context to the target app's `IntentHandler` and respond to the raising application with an `IntentResolution`: + +```javascript +{ + "intent": "StartChat", + "source": { + "appId": "Slack", + "instanceId": "e36d43e1-4fd3-447a-a227-38ec48a92706" + }, + getResult: ƒ +} +``` + +This is encoded and sent to the bridge (ommiting the `getResult()` function) as: ```JSON // agent-B -> DAB { - "type": "intentResolution", + "type": "raiseIntentResponse", "payload": { - "intent": "StartChat", - "source": { - "appId": "" - }, - "version": "...", - }, - "meta": { - "requestGuid": "requestGuid", - "responseGuid": "intentResolutionResponseGuid", - "timestamp": "2020-03-...", - "error?:": "ResolveError Enum", - "source": { //Note this was the destination of the raised intent - "appId": "", - }, - "destination": { - "app": { //note this was the source of the raised intent - "name": "someOtherApp", - "appId": "", - "version": "", - "desktopAgent": "agent-A" - // ... other metadata fields + "intentResolution": { + "intent": "StartChat", + "source": { + "appId": "Slack", + "instanceId": "e36d43e1-4fd3-447a-a227-38ec48a92706" } } + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-..." } } ``` -The bridge will fill in the `source.DesktopAgent` and relay the message on to agent-A +:::note +When producing a response to a `raiseIntent` request, the instance of the receiving application MUST be initialized and an `instanceId` generated for it before the `IntentResolution` is generated so that it may include the `instanceId`. +::: + +The bridge will fill in the `intentResolution.source.DesktopAgent` & `source.desktopAgent` and relay the message back to agent-A: ```JSON // DAB -> agent-A { - "type": "intentResolution", + "type": "raiseIntentResponse", "payload": { - "intent": "StartChat", - "source": { - "appId": "", - "desktopAgent": "agent-B" // filled by DAB - // ... other metadata fields + "intentResolution": { + "intent": "StartChat", + "source": { + "appId": "Slack", + "instanceId": "e36d43e1-4fd3-447a-a227-38ec48a92706", + "desktopAgent": "agent-B" // added by DAB + } } }, "meta": { - "requestGuid": "requestGuid", - "responseGuid": "intentResolutionResponseGuid", + "requestGuid": "", + "responseGuid": "", "timestamp": "2020-03-...", "source": { - "appId": "", - "desktopAgent": "agent-B" // filled by DAB - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "", - "desktopAgent": "agent-A" - } - } + "desktopAgent": "agent-B" // added by DAB + } } } ``` -When `AChatApp` produces a response, or the intent handler finishes running, it should send a further `intentResult` message to send that response onto the intent raiser (or throw an error if one occurred) +When `Slack` produces an `IntentResult` from its `IntentHandler`, or the intent handler finishes running without returning a result, it should send a further `raiseIntentResultResponse` message to indicate that its finished running and to pass any `IntentResult` onto the raising application. ```JSON -// agent-B -> DAB -> agent-A +// agent-B -> DAB { - "type": "intentResult", - "payload?:": { - "channel": { - "id": "channel 1", - "type": "system" - }, - "context": {/*contextObj*/} // in alternative to channel + "type": "raiseIntentResultResponse", + "payload": { + "intentResult": { + "context": {/*contextObj*/} + /* for a channel IntentResult use: + "channel": { + "id": "channel 1", + "type": "system" + } + */ + } }, "meta": { - "requestGuid": "requestGuid", - "responseGuid": "intentResultResponseGuid", + "requestGuid": "", + "responseGuid": "", //a different GUID should be used for the result response "timestamp": "2020-03-...", - "error?:": "ResultError Enum", "source": { - "appId": "", - "desktopAgent": "agent-B" // filled by DAB - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "", - "desktopAgent": "agent-A" - // ... other metadata fields - } - } + "appId": "Slack" + } } } ``` -If intent result is a channel: +:::note +If intent result is private channel see [PrivateChannels](#privatechannels) +::: +Finally, the bridge augments the response with `source.desktopAgent` and passes it back to Agent-A. -If intent result is private channel: - * see [PrivateChannels](#privatechannels) +```JSON +// DAB -> agent-A +{ + "type": "intentResult", + "payload": { + "intentResult": { + "context": {/*contextObj*/} + } + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-...", + "source": { + "appId": "Slack", + "desktopAgent": "agent-B" // added by DAB + } + } +} +``` + +If the `IntentHandler` returned `void` rather than an intent result `payload.intentResult` should be empty, e.g.: ```JSON -// agent-B -> DAB -> agent-A +// DAB -> agent-A { "type": "intentResult", - "payload?:": { - "channel": { - "id": "channel a", - "type": "private" - }, - "context": {/*contextObj*/} // in alternative to channel + "payload": { + "intentResult": {} }, "meta": { - "requestGuid": "requestGuid", - "responseGuid": "intentResultResponseGuid", + "requestGuid": "", + "responseGuid": "", "timestamp": "2020-03-...", - "error?:": "ResultError Enum", "source": { - "appId": "", - "desktopAgent": "agent-B" // filled by DAB - }, - "destination": { // duplicates the app argument - "app": { - "appId": "", - "desktopAgent": "agent-A" - } + "appId": "Slack", + "desktopAgent": "agent-B" // added by DAB } } } ``` + + + + + + + + + + --- `onSubscribe` to the private channel sent to server: @@ -1254,9 +1392,9 @@ If intent result is private channel: "app": { "name": "someOtherApp", "appId": "", - "version": "", - "desktopAgent": "agent-B" - // ... other metadata fields + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields } } } @@ -1837,4 +1975,32 @@ The bridge will add in the source agent (agent-A) and forward the message to des } } } -``` \ No newline at end of file +``` + +### Handling of error responses + +The FDC3 Desktop Agent API specifies a number of error enumerations that define specific error strings that should be used as the `message` element of a JavaScript `Error` to be returned to the requesting application via a rejected promise. + +In the event that an error occurs as part of a message exchange, Desktop Agent Bridging does NOT require that an `Error` object is returned across the bridge as it cannot be fully recreated from its consitituent fields in JavaScript. Rather, return only the specified message string in the `error` field of the `payload`, which should then be used to initialize a JavaScript `Error` on the receiving end. It is also advisable to output additional logging indicating that the error was originally generated by a remote Desktop Agent and to provide the relevant details. + +Message exchange: + +```JSON +// e.g. agent-B -> DAB in response to a raiseIntent call +{ + "type": "raiseIntentResponse", + "payload": { + "error": "TargetInstanceUnavailable", //", + "responseGuid": "", + "timestamp": "2020-03-...", + "source": { + "desktopAgent": "agent-B" // agent that generated the error, e.g. destination of the raised intent + } + } +} +``` + +In the event that an error is generated in response to a 'Request Response (collated)' type message exchange, where some agents produced successful responses, then the error MUST NOT be returned to the requesting agent and the collation of the successful responses returned instead. Desktop Agent Bridge implementations and the Desktop Agent that was the source of the error SHOULD log the errors received for debugging purposes. \ No newline at end of file From 5471c3c7074b3c097ec76913aab0f22eb28be428 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Wed, 28 Sep 2022 09:41:38 +0100 Subject: [PATCH 49/61] cleanup --- docs/api-bridging/spec.md | 113 +++++++++++++++----------------------- 1 file changed, 45 insertions(+), 68 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 44cae27a2..d3376807c 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -217,7 +217,7 @@ If requested by the server, the JWT auth token payload should take the form: ```typescript { - "sub": string, // UUID for the keypair used to sign the token + "sub": string, // GUID for the keypair used to sign the token "iat": date // timestamp at which the the token was generated as specified in ISO 8601 } ``` @@ -231,7 +231,7 @@ e.g. } ``` -Note that the `sub` SHOULD be a UUID that does NOT need to match the name requested by the Desktop Agent. It will be used to identify the keypair that should be used to validate the JWT token. Further, multiple Desktop Agent's MAY share the same keys for authentication and hence the same `sub`, but they will be assigned different names for routing purposes by the DAB. If an agent disconnects from the bridge and later re-connects it MAY request and be assigned the same name it connected with before. +Note that the `sub` SHOULD be a GUID that does NOT need to match the name requested by the Desktop Agent. It will be used to identify the keypair that should be used to validate the JWT token. Further, multiple Desktop Agent's MAY share the same keys for authentication and hence the same `sub`, but they will be assigned different names for routing purposes by the DAB. If an agent disconnects from the bridge and later re-connects it MAY request and be assigned the same name it connected with before. ### Step 4. Auth Confirmation and Name Assignment @@ -245,7 +245,7 @@ The DAB will extract the authentication token `sub` from the JWT token's claims timestamp: date, /** GUID for the handshake request */ requestGuid: string, - /** Unique guid for this message */ + /** Unique GUID for this message */ responseGuid: string, } } @@ -308,7 +308,7 @@ The `connectedAgentsUpdate` message will take the form: * Should be the same as the responseGuid for a disconnection. */ requestGuid: string, - /** Unique guid for this message */ + /** Unique GUID for this message */ responseGuid: string, /** Timestamp at which response was generated */ timestamp: date, @@ -717,7 +717,7 @@ which it repeats on to agent-B AND agent-C with the `source.desktopAgent` metada "context": { /*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": [{ "appId": "agentA-app1", @@ -771,7 +771,7 @@ Outward message to the DAB: "context": {/*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": [{ "appId": "agentA-app1", @@ -988,7 +988,7 @@ Outward message to the DAB: "context": {/*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "agentA-app1", @@ -1410,7 +1410,7 @@ If the `IntentHandler` returned `void` rather than an intent result `payload.int "type": "privateChannelSubscribe", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "name": "AChatApp", @@ -1439,22 +1439,19 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "privateChannelSubscribe", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { - "name": "AChatApp", - "appId": "", - "version": "", + "appId": "AChatApp", "desktopAgent": "agent-A" // ... other metadata fields }, "destination": { // duplicates the app argument "app": { - "name": "someOtherApp", - "appId": "", - "version": "", - "desktopAgent": "agent-B" - // ... other metadata fields + "appId": "someOtherApp", + "version": "", + "desktopAgent": "agent-B" + // ... other metadata fields } } } @@ -1470,19 +1467,15 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "privateChannelUnsubscribe", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { - "name": "AChatApp", - "appId": "", - "version": "", + "appId": "AChatApp", // ... other metadata fields }, "destination": { // duplicates the app argument "app": { - "name": "someOtherApp", - "appId": "", - "version": "", + "appId": "someOtherApp", "desktopAgent": "agent-B" // ... other metadata fields } @@ -1496,23 +1489,19 @@ The bridge will add in the source agent (agent-A) and forward the message to des ```JSON // DAB -> agent-B { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "type": "privateChannelUnsubscribe", "payload": {}, "meta": { "source": { - "name": "AChatApp", - "appId": "", - "version": "", + "appId": "AChatApp", "desktopAgent": "agent-A", // ... other metadata fields }, "destination": { // duplicates the app argument "app": { - "name": "someOtherApp", - "appId": "", - "version": "", + "appId": "someOtherApp", "desktopAgent": "agent-B" // ... other metadata fields } @@ -1530,19 +1519,15 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "privateChannelDisconnect", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { - "name": "AChatApp", - "appId": "", - "version": "", + "appId": "AChatApp", // ... other metadata fields }, "destination": { // duplicates the app argument "app": { - "name": "someOtherApp", - "appId": "", - "version": "", + "appId": "someOtherApp", "desktopAgent": "agent-B" // ... other metadata fields } @@ -1559,20 +1544,16 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "privateChannelDisconnect", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { - "name": "AChatApp", - "appId": "", - "version": "", + "appId": "AChatApp", "desktopAgent": "agent-A" // ... other metadata fields }, "destination": { // duplicates the app argument "app": { - "name": "someOtherApp", - "appId": "", - "version": "", + "appId": "someOtherApp", "desktopAgent": "agent-B" // ... other metadata fields } @@ -1670,15 +1651,13 @@ It sends an outward message to the bridge: "type": "open", "payload": { "AppIdentifier": { - "name": "myApp", - "appId": "myApp-v1.0.1", - "version": "1.0.1", + "appId": "myApp", "desktopAgent":"agent-B" }, "context": {/*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", } } @@ -1692,15 +1671,13 @@ which is repeated as: "type": "open", "payload": { "AppIdentifier": { - "name": "myApp", - "appId": "myApp-v1.0.1", - "version": "1.0.1", - "desktopAgent":"DesktopAgentB" + "appId": "myApp", + "desktopAgent":"DesktopAgentB" }, "context": {/*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": 2020-03-..., "source": { "desktopAgent": "agent-A", // filled by DAB @@ -1749,7 +1726,7 @@ Hence, the broadcast message should be modified to: "context": { /*contextObj*/} }, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "desktopAgent": "agent-A", @@ -1776,8 +1753,8 @@ Hence, the broadcast message should be modified to: "context": {/*contextObj*/} // in alternative to channel }, "meta": { - "requestGuid": "requestGuid", - "responseGuid": "intentResultResponseGuid", + "requestGuid": "", + "responseGuid": "", "timestamp": "2020-03-...", "error?:": "ResultError Enum", "source": { @@ -1787,9 +1764,9 @@ Hence, the broadcast message should be modified to: }, "destination": { // duplicates the app argument "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-A" - // ... other metadata fields + "appId": "someOtherApp", + "desktopAgent": "agent-A" + // ... other metadata fields } } } @@ -1804,7 +1781,7 @@ Hence, the broadcast message should be modified to: "type": "PrivateChannel.onAddContextListener", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", @@ -1829,7 +1806,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "PrivateChannel.onAddContextListener", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", @@ -1855,7 +1832,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "PrivateChannel.onUnsubscribe", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp" @@ -1880,7 +1857,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "PrivateChannel.onUnsubscribe", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", @@ -1906,7 +1883,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "PrivateChannel.onDisconnect", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", @@ -1931,7 +1908,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "PrivateChannel.onDisconnect", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", @@ -1960,7 +1937,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "privateChannelDisconnect", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", @@ -1985,7 +1962,7 @@ The bridge will add in the source agent (agent-A) and forward the message to des "type": "privateChannelDisconnect", "payload": {}, "meta": { - "requestGuid": "requestGuid", + "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", From 41a8e2064747c094d152e936fd760bc0a3cbebd1 Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 28 Sep 2022 13:48:07 +0100 Subject: [PATCH 50/61] Further clean-up and adding messages for PrivateChannel events and broadcast --- docs/api-bridging/spec.md | 420 +++++--------------------------------- 1 file changed, 55 insertions(+), 365 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index d3376807c..6c20907cb 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1342,7 +1342,7 @@ When `Slack` produces an `IntentResult` from its `IntentHandler`, or the intent ``` :::note -If intent result is private channel see [PrivateChannels](#privatechannels) +If intent result is private channel see [PrivateChannels](#privatechannels) for additional message exchanges hat may be needed. ::: Finally, the bridge augments the response with `sources[0].desktopAgent` and passes it back to Agent-A. @@ -1395,173 +1395,7 @@ If the `IntentHandler` returned `void` rather than an intent result `payload.int - - - - - - ---- -`onSubscribe` to the private channel sent to server: - -```JSON -// agent-A -> DAB -{ - "type": "privateChannelSubscribe", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "name": "AChatApp", - "appId": "", - "version": "", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "name": "someOtherApp", - "appId": "", - "version": "", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) - -```JSON -// DAB -> agent-B -{ - "type": "privateChannelSubscribe", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - "desktopAgent": "agent-A" - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "version": "", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - ---- -`onUnsubscribe` to the private channel sent to the bridge - -```JSON -// agent-A -> DAB -{ - "type": "privateChannelUnsubscribe", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) - -```JSON -// DAB -> agent-B -{ - "requestGuid": "", - "timestamp": "2020-03-...", - "type": "privateChannelUnsubscribe", - "payload": {}, - "meta": { - "source": { - "appId": "AChatApp", - "desktopAgent": "agent-A", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - ---- -`onDisconnect` to the private channel sent to the bridge - -```JSON -// agent-A -> DAB -{ - "type": "privateChannelDisconnect", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) - -```JSON -// DAB -> agent-B -{ - "type": "privateChannelDisconnect", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - "desktopAgent": "agent-A" - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - +//TODO: further work required on the below messsage exchanges: ### open ```typescript @@ -1650,15 +1484,19 @@ It sends an outward message to the bridge: { "type": "open", "payload": { - "AppIdentifier": { + "app": { "appId": "myApp", "desktopAgent":"agent-B" - }, + }, "context": {/*contextObj*/} }, "meta": { "requestGuid": "", "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6" + } } } ``` @@ -1670,18 +1508,19 @@ which is repeated as: { "type": "open", "payload": { - "AppIdentifier": { + "app": { "appId": "myApp", "desktopAgent":"DesktopAgentB" - }, - "context": {/*contextObj*/} + }, + "context": {/*contextObj*/} }, "meta": { "requestGuid": "", "timestamp": 2020-03-..., "source": { - "desktopAgent": "agent-A", // filled by DAB - // ... other metadata fields + "appId": "AChatApp", + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6", + "desktopAgent": "agent-A" //added by DAB } } } @@ -1709,18 +1548,24 @@ sequenceDiagram ### PrivateChannels -`PrivateChannels` provide some additional event handlers for the addition or removal of context listeners and are intended to provide a private communication channel for applications. Hence, there is a difference in how their broadcasts SHOULD be handled and a number of additional message exchanges necessary for their events. +`PrivateChannels` are intended to provide a private communication channel for applications. In order to do so, there are differences in how their broadcasts MUST be handled and a number of additional message exchanges MUST be supported in order to handle events that are used to manage the channel's lifecycle. + +When a `ContextListener` is added to a remote `PrivateChannel`, removed from it or the `disconnect` function is called a notifications must be sent to the application that owns the channel. The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B). + +Such notification messages should be addessed to the Desktop Agent that owns the channel, which will route it to the owning application and any other listeners. If any of those listeners are remote, the message should be repeated back to the bridge, once for each listener with the destination set as a full `AppIdentifier`. Both these messages and broadcast messages MUST NOT be repeated back to the application that generated them. The source information on repeated messages should be unmodified to ensure that the message is attributed to the original source. #### `broadcast` -Desktop Agents SHOULD provide special handling for `PrivateChannel` broadcasts. A copy of the broadcast message SHOULD be forwarded to the bridge for each `ContextListener` added by a remote Desktop Agent, with a `destination` field set. Doing so will require the Desktop Agent (in addition to the app that owns the `PrivateChannel`) to track which applications have added a `ContextListener` to the `PrivateChannel`. +Type: **Request only** + +To maintain the privacy of PrivateChannel broadcasts, both Desktop Agents and Desktop Agent Bridge implementations SHOULD provide special handling for `PrivateChannel` broadcasts. Rather than broadcasting messages to all other Desktop Agents, broadcasts should be sent only to the Desktop Agent that owns the PrivateChannel, which will then repeat the broadcast message on to each `ContextListener` (both those that are local to the Desktop Agent and those added by remote Desktop Agents) - except for the source application. Doing so will require the Desktop Agent to track which applications have added a `ContextListener` to the `PrivateChannel`. -Hence, the broadcast message should be modified to: +Hence, the broadcast message should be modified such that it includes a destination and the `type` should indicate that it is a `PrivateChannel` broadcast: ```JSON // DAB -> agent-B { - "type": "broadcast", + "type": "PrivateChannel.broadcast", //modified type for PrivateChannel broadcasts "payload": { "channel": "private-channel-ABC123", "context": { /*contextObj*/} @@ -1729,96 +1574,43 @@ Hence, the broadcast message should be modified to: "requestGuid": "", "timestamp": "2020-03-...", "source": { - "desktopAgent": "agent-A", - "appId": "..." + "appId": "AChatApp", + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6", + "desktopAgent": "agent-A" //added by DAB }, "destination": { - + "desktopAgent": "agent-B" } } } ``` -//TODO intent result is a private channel - -```JSON -// agent-B -> DAB -> agent-A -{ - "type": "intentResult", - "payload?:": { - "channel": { - "id": "channel a", - "type": "private" - }, - "context": {/*contextObj*/} // in alternative to channel - }, - "meta": { - "requestGuid": "", - "responseGuid": "", - "timestamp": "2020-03-...", - "error?:": "ResultError Enum", - "source": { - "appId": "AChatApp", - "desktopAgent": "agent-B" // filled by DAB - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-A" - // ... other metadata fields - } - } - } -} -``` +For a broadcast to a `PrivateChannel` owned by another agent the detination should just include the `DesktopAgentIdentifier`. The message should then be repeated (once for each listener) from that Desktop Agent to other remote listeners without modifying the source information (to ensure that the message is attributed to the original source), but with a destination set as a full `AppIdentifer` for each remote listener. #### `onAddContextListener` -```JSON -// agent-A -> DAB -{ - "type": "PrivateChannel.onAddContextListener", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` +Type: **Request only** -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) +When a `ContextListener` is added to a remote `PrivateChannel` or a remote listener needs to be notified of the event use the following message: ```JSON -// DAB -> agent-B +// agent-A -> DAB { "type": "PrivateChannel.onAddContextListener", - "payload": {}, + "payload": { + "channel": "private-channel-ABC123" + }, "meta": { "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", - "desktopAgent": "agent-A" - // ... other metadata fields + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6", + "desktopAgent": "agent-A" //added by DAB }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } + "destination": { + "desktopAgent": "agent-B" //use DesktopAgentIdentifier to notify a remote PrivateChannel + //use a full AppIdentifier for event notifications to applications } } } @@ -1826,30 +1618,9 @@ The bridge will add in the source agent (agent-A) and forward the message to des #### `listener.unsubscribe` -```JSON -// agent-A -> DAB -{ - "type": "PrivateChannel.onUnsubscribe", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp" - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` +Type: **Request only** -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) +When a `ContextListener` is unsubscribed from a remote `PrivateChannel` or a remote listener needs to be notified of the event use the following message: ```JSON // DAB -> agent-B @@ -1861,121 +1632,40 @@ The bridge will add in the source agent (agent-A) and forward the message to des "timestamp": "2020-03-...", "source": { "appId": "AChatApp", - "desktopAgent": "agent-A", - // ... other metadata fields + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6", + "desktopAgent": "agent-A" //added by DAB }, "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } + "desktopAgent": "agent-B" //use DesktopAgentIdentifier to notify a remote PrivateChannel + //use a full AppIdentifier for event notifications to applications + } } } ``` #### `onDisconnect` -```JSON -// agent-A -> DAB -{ - "type": "PrivateChannel.onDisconnect", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) - -```JSON -// DAB -> agent-B -{ - "type": "PrivateChannel.onDisconnect", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - "desktopAgent": "agent-A" - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - - +Type: **Request only** ---- -`onDisconnect` to the private channel sent to the bridge +When the `disconnect` function is is called on a remote `PrivateChannel` or a remote listener needs to be notified of the event use the following message: ```JSON // agent-A -> DAB { - "type": "privateChannelDisconnect", - "payload": {}, - "meta": { - "requestGuid": "", - "timestamp": "2020-03-...", - "source": { - "appId": "AChatApp", - // ... other metadata fields - }, - "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } - } -} -``` - -The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B) - -```JSON -// DAB -> agent-B -{ - "type": "privateChannelDisconnect", + "type": "PrivateChannel.onDisconnect", "payload": {}, "meta": { "requestGuid": "", "timestamp": "2020-03-...", "source": { "appId": "AChatApp", - "desktopAgent": "agent-A" - // ... other metadata fields + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6", + "desktopAgent": "agent-A" //added by DAB }, "destination": { // duplicates the app argument - "app": { - "appId": "someOtherApp", - "desktopAgent": "agent-B" - // ... other metadata fields - } - } + "desktopAgent": "agent-B" //use DesktopAgentIdentifier to notify a remote PrivateChannel + //use a full AppIdentifier for event notifications to applications + } } } ``` From 4fa0b80f11b357e889f172c952d5ed8b424e2b98 Mon Sep 17 00:00:00 2001 From: Kris West Date: Mon, 10 Oct 2022 18:16:56 +0100 Subject: [PATCH 51/61] Improving a poorly worded sentence --- docs/api-bridging/spec.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 6c20907cb..beb189085 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -333,7 +333,7 @@ This procedure is the same for both previously connected and connecting agents, After applying the `connectedAgentsUpdate` message, the newly connected Desktop Agent and other already connected agents are able to begin communicating through the bridge. -The handling of these synchronization messages from the DAB to Desktop Agents should be handled atomically by Desktop Agents to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. I.e. the `connectedAgentsUpdate` message must be processed immediately on receipt and updates applied before any other messages are sent or responses processed. +Handling by the Desktop Agent of these synchronization messages from the DAB should be atomic to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. I.e. the `connectedAgentsUpdate` message must be processed immediately on receipt by Desktop Agents and updates applied before any other messages are sent or responses processed. Similarly, the DAB must process `handshake` messages and issue `connectedAgentsUpdate` messages to all participants (steps 3-6) atomically, allowing no overlap with the processing of other messages from connected agents. From ac45778c9756e78ac00a3d2707d05fb8dfb29b04 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 11:26:39 +0100 Subject: [PATCH 52/61] Add advice on visual feedback to users on connect events --- docs/api-bridging/spec.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index beb189085..c86ff1d51 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -15,6 +15,7 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Exempted PrivateChannels from some cases where bridge messages are not normally generated (but should be for PrivateChannels). * Classified message exchanges by type. * Completed message exchanges for broadcast, findIntent, findIntentsForContext and raiseIntent. +* Added advice on whether other agents report to users on connect/disconnect events? (SHOULD) ## Open questions @@ -26,7 +27,7 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Complete message exchange documentation * Expand on how the DAB should create the JWT token (and its claims, which must change to avoid replay attacks) which it sends out in the `hello` message for DAs to validate. -* Advise on whether other agents report to users on connect/disconnect events? (SHOULD) + * How to handle events from PrivateChannels (addContextListener, listener.unsubscribe, disconnect) * To create final PR: * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction @@ -316,11 +317,11 @@ The `connectedAgentsUpdate` message will take the form: } ``` -When an agent connects to the bridge, it should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast as the context being merged would become the most recent context on the channel, when other contexts may have been broadcast subsequently. Rather, it should be adopted internally by the Desktop Agent merging it such that it would be received by applications that are adding a user channel context listener or calling `channel.getCurrentContext()`. +When an agent connects to the bridge, it and other agents connected to the bridge, should adopt the state of any channels that do not currently exist or do not currently contain state of a particular type. This synchronization is NOT performed via broadcast as the context being merged would become the most recent context on the channel, when other contexts may have been broadcast subsequently. Rather, it should be adopted internally by each Desktop Agent, merging it such that it would be received by applications that have added a context listener to the channel or call `channel.getCurrentContext()` on it. It should be noted that Desktop Agents will not have context listeners for previously unknown channels, and SHOULD simply record that channel's state for use when that channel is first used. -For known channel names, the Desktop Agent MUST also compare its current state to that which it has just received and may need to internally adopt context of types not previously seen on a channel. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as only the most recently transmitted Context should be received by un-typed listeners). Hence, the new context MUST only be passed to a context listener if it was registered specifically for that type and a context of that type did not previously exist on the channel. +For known channel names, the Desktop Agents MUST also compare their current state to that which they have just received and may need to internally adopt context of types not previously seen on a channel. As context listeners can be registered for either a specific type or all types some care is necessary when doing so (as only the most recently transmitted Context should be received by un-typed listeners). Hence, the new context MUST only be passed to a context listener if it was registered specifically for that type and a context of that type did not previously exist on the channel. In summary, updating listeners for a known channel should be performed as follows: @@ -333,14 +334,22 @@ This procedure is the same for both previously connected and connecting agents, After applying the `connectedAgentsUpdate` message, the newly connected Desktop Agent and other already connected agents are able to begin communicating through the bridge. -Handling by the Desktop Agent of these synchronization messages from the DAB should be atomic to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. I.e. the `connectedAgentsUpdate` message must be processed immediately on receipt by Desktop Agents and updates applied before any other messages are sent or responses processed. +#### Atomicity and handling concurrent operations + +Handling by the Desktop Agent of the synchronization message from the DAB in step 6 of the connection protocol should be atomic to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. I.e. the `connectedAgentsUpdate` message must be processed immediately on receipt by Desktop Agents and updates applied before any other messages are sent or responses processed. + +Similarly, the Desktop Agent Bridge must process steps 3-6 of the connection protocol (receiving a`handshake` messages upto issuing the `connectedAgentsUpdate` messages to all participants) as a single atomic unit, allowing no overlap with the processing of other messages from connected agents (as they might modify the state information it is processing during those steps). -Similarly, the DAB must process `handshake` messages and issue `connectedAgentsUpdate` messages to all participants (steps 3-6) atomically, allowing no overlap with the processing of other messages from connected agents. +#### Notification to users of connection events +Desktop Agents SHOULD provide visual feedback to end users when they or other agents connect or disconnect from the Destkop Agent Bridge (i.e. whenever a `connectedAgentsUpdate` message is received). Doing so will ensure that the end user understands whether their apps and Desktop Agent can communicate with other apps running under other Desktop Agents, and can better attribute any issues with interoperability between them to the probable source. ### Step 7. Disconnects Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 6 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. + + + ## Messaging Protocol In order for Desktop Agents to communicate with the Desktop Agent Bridge and thereby other Desktop Agents, a messaging protocol is required. FDC3 supports both 'fire and forget' interactions (such as the broadcast of context messages) and interactions with specific responses (such as raising intents and returning a resolution and optional result), both of which must be handled by that messaging protocol and message formats it defines, as described in this section. From d7cecea88125971b634f6e7ec7e90560fc24d6d7 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 11:31:42 +0100 Subject: [PATCH 53/61] Handle TODO in Forwarding of Messages and Collating Responses --- docs/api-bridging/spec.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index c86ff1d51..c5d499a53 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -545,7 +545,7 @@ The following pseudo-code defines how messages should be forwarded or collated b * annotate the request as requiring responses from all other connected agents, * await responses or the specified timeout. * else if a `meta.destination` was included, - * forward it to the specified destination agent + * forward it to the specified destination agent, * annotate the request as requiring only a response from the specified agent, * await the response or the specified timeout. * else if the message is a response (both `meta.requestGuid` and `meta.responseGuid` are set) @@ -558,16 +558,14 @@ The following pseudo-code defines how messages should be forwarded or collated b * produce the collated response message and return to the requesting Desktop Agent. * else await the configured response timeout or further responses, * if the timeout is reached without any responses being received - * produce and return an appropriate [error response](../api/ref/Errors), including details of all Desktop Agents in `timedOutSources`. + * produce and return an appropriate [error response](../api/ref/Errors), including details of all Desktop Agents in `errorSources`. * log the timeout for each Desktop Agent that did not respond and check disconnection criteria. * if the timeout is reached with a partial set of responses - * produce and return a collated response, but include details of Desktop Agents that timed out in `timedOutResponses`. + * produce and return a collated response, but include details of Desktop Agents that timed out in `errorSources`. * log the timeout for each Desktop Agent that did not respond and check disconnection criteria. * else discard the response message (as it is a delayed to a request that has timed out or is otherwise invalid). * else the message is invalid and should be discarded. -//TODO add detail on DA responsibilities and how requests should be handled by the desktop agent (with timeouts and messaging to user on timedOutSources) - ### Workflows Broken By Disconnects Targeted request and request/response workflows may be broken when a Desktop Agent disconnects from the bridge, which bridge implementations will need to handle. From 8816731f0a1253cfa7d199c00e58caca00176a8f Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 11:42:22 +0100 Subject: [PATCH 54/61] Added section on returning error responses --- docs/api-bridging/spec.md | 55 +++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index c5d499a53..b66900bf5 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -519,6 +519,36 @@ Response messages do not include a `destination` field. Instead, a Desktop Agent Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of any such augmentation are provided in the description of each message exchange. +### Returning Errors +In the event that an Error must be returned by a Desktop Agent, it should be selected from the [Error enumeration](../api/ref/Errors) normally used by the corresponding FDC3 function (i.e. `OpenError` for `open` calls, `ResolveError` for `findIntent` and `raiseIntent` etc.). For messages that target a specific agent this will result in an error being returned to the calling agent and onto the application that made teh original request (as defined in the response message format). + +However, API calls that require a collated response from all agents where at least one agent returns a successful respose, will result in a successful response from the Desktop Agent Bridge (i.e. no `error` element should be included), with the agents returning errors listed in the `errorSources` array. This allows for successful exchanges on API calls such as `fdc3.raiseIntent` where some agents do not have options to return and would normally respond with (for example) `ResolveError.NoAppsFound`. + +Finally, to facilitate easier debugging, errors specific to Desktop Agent Bridge are added to those enumerations, including: + +```typescript +enum OpenError { + ... + /** Returned if the specified Desktop Agent is not found, via a connected + Desktop Agent Bridge. */ + DesktopAgentNotFound = 'DesktopAgentNotFound', +} + +enum ResolveError { + ... + /** Returned if the specified Desktop Agent is not found, via a connected + Desktop Agent Bridge. */ + DesktopAgentNotFound = 'DesktopAgentNotFound', +} + +enum ResultError { + ... + /** Returned if the specified Desktop Agent disconnected from the Desktop + Agent Bridge before a result was returned. */ + DesktopAgentDisconnected = 'DesktopAgentDisconnected', +} +``` + ### Forwarding of Messages and Collating Responses When handling request messages, it is the responsibility of the Desktop Agent Bridge to: @@ -587,31 +617,6 @@ When processing the disconnection of an agent from the bridge, the bridge MUST e * For requests that target a specific agent: * return an appropriate error (as the request cannot be completed). -In the event that an Error must be returned (for requests that target a specific agent), it should be selected from the [Error enumeration](../api/ref/Errors) normally used by the corresponding FDC3 function (i.e. `OpenError` for `open` calls, `ResolveError` for `findIntent` and `raiseIntent` etc.). To facilitate easier debugging, errors specific to Desktop Agent Bridge are added to those enumerations, including: - -```typescript -enum OpenError { - ... - /** Returned if the specified Desktop Agent is not found, via a connected - Desktop Agent Bridge. */ - DesktopAgentNotFound = 'DesktopAgentNotFound', -} - -enum ResolveError { - ... - /** Returned if the specified Desktop Agent is not found, via a connected - Desktop Agent Bridge. */ - DesktopAgentNotFound = 'DesktopAgentNotFound', -} - -enum ResultError { - ... - /** Returned if the specified Desktop Agent disconnected from the Desktop - Agent Bridge before a result was returned. */ - DesktopAgentDisconnected = 'DesktopAgentDisconnected', -} -``` - Finally, in the event that either a Desktop Agent or the bridge itself stops responding, but doesn't fully disconnect, the timeouts (specified earlier in this document) will be used to handle the request as if a disconnection had occurred. In the event that a Desktop Agent repeatedly times out, the bridge SHOULD disconnect that agent (and update other agents via the `connectedAgentsUpdate` message specified in the connection protocol), to avoid all requests requiring the full timeout to complete. From 63d5978ebca1fd20f79861fae93cc0455828f201 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 11:58:37 +0100 Subject: [PATCH 55/61] Correction to raiseIntent message exchange --- docs/api-bridging/spec.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index b66900bf5..2d613561d 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -16,6 +16,7 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Classified message exchanges by type. * Completed message exchanges for broadcast, findIntent, findIntentsForContext and raiseIntent. * Added advice on whether other agents report to users on connect/disconnect events? (SHOULD) +* Added advice on handling error responses from Desktop Agents ## Open questions @@ -27,7 +28,6 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Complete message exchange documentation * Expand on how the DAB should create the JWT token (and its claims, which must change to avoid replay attacks) which it sends out in the `hello` message for DAs to validate. - * How to handle events from PrivateChannels (addContextListener, listener.unsubscribe, disconnect) * To create final PR: * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction @@ -1149,7 +1149,7 @@ Each `AppMetadata` object is augmented by the bridge with a `desktopAgent` field Finally agent-A combines the payload received with it own response and returns it to the requesting application. -### raiseIntent +### raiseIntent (on `fdc3`) Type: **Request Multiple Response (single)** @@ -1157,7 +1157,7 @@ Generated by API call: * [`raiseIntent(intent, context, app)`](../api/ref/DesktopAgent#raiseIntent) -For Desktop Agent bridging, a `raiseIntent` call MUST always pass a `app: TargetApp` argument. If one is not passed, then the `findIntent` message exchange should be used to collect options for the local resolver to use. Once an option has been selected (for example because there is only one option, or because the user selected an option in a local intent resolver UI), the `raiseIntent` message exchange may then be used (if a remote option was selected as the resolution) to raise the intent. +For Desktop Agent Bridging, a `fdc3.raiseIntent` call MUST always pass a `app: AppIdentifier` argument to target the intent. If a target `app` is not passed, then the `findIntent` message exchange should be used to collect options for the local resolver to use (note that Desktop Agents MAY also support the deprecated `raiseIntent` signature that uses the app `name` field by using the `findIntent` message exchange to attempt to resolve the `name` to an `AppIdentifier`). Once an option has been selected (for example because there is only one option, or because the user selected an option in a local intent resolver UI), the `raiseIntent` message exchange may then be used (if a remote option was selected as the resolution) to raise the intent. e.g. An application with appId `agentA-app1` makes the following API call: @@ -1174,7 +1174,7 @@ let appIntent = await fdc3.raiseIntent("StartChat", context, {"appId": "Slack", Message exchange: :::note -Agent-C is not involved in the disagram below as the `raiseIntent` is now specified withtarget applicaiton and Desktop Agent. +Agent-C is not involved in the diagram below as the `raiseIntent` is now specified with target application and Desktop Agent. ::: ```mermaid @@ -1204,7 +1204,8 @@ Outward message to the DAB: "context": {/*contextObj*/}, "app": { // AppIdentifier for chosen resolution including desktopAgent value "appId": "Slack", - "desktopAgent": "agent-B" + "desktopAgent": "agent-B" + //Note an instanceId may be included to target an already running instance } }, "meta": { @@ -1354,7 +1355,7 @@ When `Slack` produces an `IntentResult` from its `IntentHandler`, or the intent ``` :::note -If intent result is private channel see [PrivateChannels](#privatechannels) for additional message exchanges hat may be needed. +If intent result is private channel see [PrivateChannels](#privatechannels) for additional message exchanges that may be needed. ::: Finally, the bridge augments the response with `sources[0].desktopAgent` and passes it back to Agent-A. @@ -1362,7 +1363,7 @@ Finally, the bridge augments the response with `sources[0].desktopAgent` and pas ```JSON // DAB -> agent-A { - "type": "intentResult", + "type": "raiseIntentResultResponse", "payload": { "intentResult": { "context": {/*contextObj*/} @@ -1386,7 +1387,7 @@ If the `IntentHandler` returned `void` rather than an intent result `payload.int ```JSON // DAB -> agent-A { - "type": "intentResult", + "type": "raiseIntentResultResponse", "payload": { "intentResult": {} }, @@ -1686,7 +1687,7 @@ When the `disconnect` function is is called on a remote `PrivateChannel` or a re The FDC3 Desktop Agent API specifies a number of error enumerations that define specific error strings that should be used as the `message` element of a JavaScript `Error` to be returned to the requesting application via a rejected promise. -In the event that an error occurs as part of a message exchange, Desktop Agent Bridging does NOT require that an `Error` object is returned across the bridge as it cannot be fully recreated from its consitituent fields in JavaScript. Rather, return only the specified message string in the `error` field of the `payload`, which should then be used to initialize a JavaScript `Error` on the receiving end. It is also advisable to output additional logging indicating that the error was originally generated by a remote Desktop Agent and to provide the relevant details. +In the event that an error occurs as part of a message exchange, Desktop Agent Bridging does NOT require that an `Error` object is returned across the bridge as it cannot be fully recreated from its constituent fields in JavaScript. Rather, return only the specified message string in the `error` field of the `payload`, which should then be used to initialize a JavaScript `Error` on the receiving end. It is also advisable to output additional logging indicating that the error was originally generated by a remote Desktop Agent and to provide the relevant details. Message exchange: From a5c0bdbf8d109c53f53c49e8202fea5c0cf1056f Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 12:08:27 +0100 Subject: [PATCH 56/61] Consolidating error handling sections --- docs/api-bridging/spec.md | 53 ++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 2d613561d..747e12663 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -16,7 +16,7 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Classified message exchanges by type. * Completed message exchanges for broadcast, findIntent, findIntentsForContext and raiseIntent. * Added advice on whether other agents report to users on connect/disconnect events? (SHOULD) -* Added advice on handling error responses from Desktop Agents +* Added/consolidated advice on handling error responses from Desktop Agents in Messaging protocol section ## Open questions @@ -519,8 +519,27 @@ Response messages do not include a `destination` field. Instead, a Desktop Agent Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of any such augmentation are provided in the description of each message exchange. -### Returning Errors -In the event that an Error must be returned by a Desktop Agent, it should be selected from the [Error enumeration](../api/ref/Errors) normally used by the corresponding FDC3 function (i.e. `OpenError` for `open` calls, `ResolveError` for `findIntent` and `raiseIntent` etc.). For messages that target a specific agent this will result in an error being returned to the calling agent and onto the application that made teh original request (as defined in the response message format). +### Handling of Error Responses +The FDC3 Desktop Agent API specifies a number of error enumerations that define specific error strings that should be used as the `message` element of a JavaScript `Error` to be returned to the requesting application via a rejected promise. In the event that an Error must be returned by a Desktop Agent to the Desktop Agent Bridge, the message should be selected from the [Error enumeration](../api/ref/Errors) normally used by the corresponding FDC3 function (i.e. `OpenError` for `open` calls, `ResolveError` for `findIntent` and `raiseIntent` etc.). However, Desktop Agent Bridging does NOT require that an `Error` object is returned across the bridge as it cannot be fully recreated from its constituent fields in JavaScript. Rather, return only the specified message string in the `error` field of the `payload`, which should then be used to initialize a JavaScript `Error` on the receiving end. It is also advisable to output additional logging (in the Desktop Agent Bridge) indicating that the error was originally generated by a remote Desktop Agent and to provide the relevant details. + +For example, a `raiseIntent` targetted at an app instane that no longer exists might generate the following response from the Desktop Agent: + +```JSON +// e.g. agent-B -> DAB in response to a raiseIntent call +{ + "type": "raiseIntentResponse", + "payload": { + "error": "TargetInstanceUnavailable", //", + "responseGuid": "", + "timestamp": "2020-03-..." + } +} +``` + +For messages that target a specific agent, the Destkop Agent Bridge will augment the message with a `source` field and return it to the calling agent and the appl that made the original request. However, API calls that require a collated response from all agents where at least one agent returns a successful respose, will result in a successful response from the Desktop Agent Bridge (i.e. no `error` element should be included), with the agents returning errors listed in the `errorSources` array. This allows for successful exchanges on API calls such as `fdc3.raiseIntent` where some agents do not have options to return and would normally respond with (for example) `ResolveError.NoAppsFound`. @@ -1682,31 +1701,3 @@ When the `disconnect` function is is called on a remote `PrivateChannel` or a re } } ``` - -### Handling of error responses - -The FDC3 Desktop Agent API specifies a number of error enumerations that define specific error strings that should be used as the `message` element of a JavaScript `Error` to be returned to the requesting application via a rejected promise. - -In the event that an error occurs as part of a message exchange, Desktop Agent Bridging does NOT require that an `Error` object is returned across the bridge as it cannot be fully recreated from its constituent fields in JavaScript. Rather, return only the specified message string in the `error` field of the `payload`, which should then be used to initialize a JavaScript `Error` on the receiving end. It is also advisable to output additional logging indicating that the error was originally generated by a remote Desktop Agent and to provide the relevant details. - -Message exchange: - -```JSON -// e.g. agent-B -> DAB in response to a raiseIntent call -{ - "type": "raiseIntentResponse", - "payload": { - "error": "TargetInstanceUnavailable", //", - "responseGuid": "", - "timestamp": "2020-03-...", - "source": { - "desktopAgent": "agent-B" // agent that generated the error, e.g. destination of the raised intent - } - } -} -``` - -In the event that an error is generated in response to a 'Request Response (collated)' type message exchange, where some agents produced successful responses, then the error MUST NOT be returned to the requesting agent and the collation of the successful responses returned instead. Desktop Agent Bridge implementations and the Desktop Agent that was the source of the error SHOULD log the errors received for debugging purposes. From 28564cb0d52d5103bec9737bdb67fd5688acb0e8 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 13:30:35 +0100 Subject: [PATCH 57/61] Adding exchanges for open and findInstances --- docs/api-bridging/spec.md | 340 ++++++++++++++++++++++++++++---------- 1 file changed, 252 insertions(+), 88 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 747e12663..54a86dc4f 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -687,8 +687,8 @@ Type: **Request only** Generated by API calls: -* [`fdc3.broadcast(contextObj)`](../api/ref/DesktopAgent#broadcast) -* [`Channel.broadcast(contextObj)`](../api/ref/Channel#broadcast) +* [`fdc3.broadcast(context: Context)`](../api/ref/DesktopAgent#broadcast) +* [`Channel.broadcast(context: Context)`](../api/ref/Channel#broadcast) e.g. @@ -765,7 +765,7 @@ Type: **Request Response (collated)** Generated by API call: -* [`findIntent(intent, context)`](../api/ref/DesktopAgent#findintent) +* [`findIntent(intent: string, context: Context)`](../api/ref/DesktopAgent#findintent) e.g. An application with `appId: "agentA-app1"` and `instanceId: "c6ad5174-6f78-4582-8e96-728d93a4d7d7"` makes the following API call: @@ -995,7 +995,7 @@ Type: **Request Response (collated)** Generated by API call: -* [`findIntentsByContext(context)`](../api/ref/DesktopAgent#findintentsbycontext) +* [`findIntentsByContext(context: Context)`](../api/ref/DesktopAgent#findintentsbycontext) e.g. An application with appId `agentA-app1` makes the following API call: @@ -1161,7 +1161,10 @@ Each `AppMetadata` object is augmented by the bridge with a `desktopAgent` field "requestGuid": "", "responseGuid": "", "timestamp": "2020-03-...", - "sources": [{ "desktopAgent": "agent-B" }, { "desktopAgent": "agent-C" }] + "sources": [ + { "desktopAgent": "agent-B" }, + { "desktopAgent": "agent-C" } + ] } } ``` @@ -1174,7 +1177,7 @@ Type: **Request Multiple Response (single)** Generated by API call: -* [`raiseIntent(intent, context, app)`](../api/ref/DesktopAgent#raiseIntent) +* [`raiseIntent(intent: string, context: Context, app: AppIdentifier)`](../api/ref/DesktopAgent#raiseIntent) For Desktop Agent Bridging, a `fdc3.raiseIntent` call MUST always pass a `app: AppIdentifier` argument to target the intent. If a target `app` is not passed, then the `findIntent` message exchange should be used to collect options for the local resolver to use (note that Desktop Agents MAY also support the deprecated `raiseIntent` signature that uses the app `name` field by using the `findIntent` message exchange to attempt to resolve the `name` to an `AppIdentifier`). Once an option has been selected (for example because there is only one option, or because the user selected an option in a local intent resolver UI), the `raiseIntent` message exchange may then be used (if a remote option was selected as the resolution) to raise the intent. @@ -1204,10 +1207,10 @@ sequenceDiagram participant DC as Desktop Agent C DA ->>+ DAB: raiseIntent DAB ->>+ DB: raiseIntent - DB -->>- DAB: intentResolution - DAB -->>- DA: intentResolution - DB ->>+ DAB: intentResult - DAB ->>+ DA: intentResult + DB -->>- DAB: raiseIntentResponse + DAB -->>- DA: raiseIntentResponse + DB ->>+ DAB: raiseIntentResultResponse + DAB ->>+ DA: raiseIntentResultResponse ``` #### Request format @@ -1235,10 +1238,8 @@ Outward message to the DAB: "instanceId": "c6ad5174-6f78-4582-8e96-728d93a4d7d7" }, "destination": { // duplicates the app argument so that the message is routed like any other - "app": { - "appId": "Slack", - "desktopAgent": "agent-B" - } + "appId": "Slack", + "desktopAgent": "agent-B" } } } @@ -1267,12 +1268,10 @@ The bridge fills in the `source.desktopAgent` field and forwards the request to "instanceId": "c6ad5174-6f78-4582-8e96-728d93a4d7d7", "desktopAgent": "agent-A" //added by DAB }, - "destination": { - "app": { - "appId": "Slack", - "desktopAgent": "agent-B" - } - }, + "destination": { // duplicates the app argument so that the message is routed like any other + "appId": "Slack", + "desktopAgent": "agent-B" + } } } ``` @@ -1427,54 +1426,40 @@ If the `IntentHandler` returned `void` rather than an intent result `payload.int -//TODO: further work required on the below messsage exchanges: -### open - -```typescript -open(app: TargetApp, context?: Context): Promise; -``` -When receiving a response from invoking `fdc3.open` the new app instances MUST be fully initialized ie. the responding Desktop Agent will need to return an `AppIdentifier` with an `instanceId`. +### open (on `fdc3`) -#### Request format +Type: **Request Response (single)** -A `fdc3.open` call is made on agent-A. +Generated by API call: -```javascript -// Open an app without context, using the app name -let instanceMetadata = await fdc3.open('myApp'); +* [`open(app: AppIdentifier, context?: Context)`](../api/ref/DesktopAgent#open) -// Open an app without context, using an AppIdentifier object to specify the target -let AppIdentifier = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1'}; -let instanceMetadata = await fdc3.open(AppIdentifier); +e.g. +```javascript // Open an app without context, using an AppIdentifier object to specify the target and Desktop Agent -let AppIdentifier = {name: 'myApp', appId: 'myApp-v1.0.1', version: '1.0.1', desktopAgent:"DesktopAgentB"}; +let AppIdentifier = {appId: 'myApp-v1.0.1', desktopAgent:"DesktopAgentB"}; let instanceMetadata = await fdc3.open(AppIdentifier); -``` -The `fdc3.open` command should result in a single copy of the specified app being opened and its instance data returned, or an error if it could not be opened. There are two possible scenarios: +// Open an app with context, using an AppIdentifier object to specify the target and Desktop Agent +let AppIdentifier = {appId: 'myApp-v1.0.1', desktopAgent:"DesktopAgentB"}; +let instanceMetadata = await fdc3.open(AppIdentifier, contextObj); +``` -1) The Desktop Agent that the app should open on is specified -2) The Desktop Agent that the app should open on is NOT specified app +Note that it is not currently possible to identify resolve all available applications within a Desktop Agent via the FDC3 API. Hence, `fdc3.open` calls without a specified `desktopAgent` field in their `AppIdentifier`, e.g.: -The first case (target Desktop Agent is specified) is simple: +```javascript +// Open a target app via AppIdentifier, without a specified Desktop Agent +let AppIdentifier = {appId: 'myApp-v1.0.1'}; +let instanceMetadata = await fdc3.open(AppIdentifier); +``` -* If the local Desktop Agent is the target, handle the call normally -* Otherwise: - * Request is sent to the bridge - * DAB checks to see if any of the connected DAs is the target and transmit the call to it and awaits a response - * otherwise return `OpenError.AppNotFound` +should always be processed locally without be passed to the bridge. -The second case is a little trickier as we don't know which agent may have the app available: +The `fdc3.open` command should result in a single copy of the specified app being opened and its instance data returned, or an error if it could not be opened. When receiving a response from invoking `fdc3.open` via the Desktop Agent Bridge, the new app instances MUST be initialized before responding as the responding Desktop Agent will need to return an `AppIdentifier` with an `instanceId` field set. -* If the local Desktop Agent has the app, open it and exit. -* Otherwise: - * Request is sent to the bridge - * Bridge will query each connected DA asynchronously and await a response - * If the response is `AppIdentifier` then return it and exit (ignore every subsequent response) - * If the response is `OpenError.AppNotFound` and there are pending responses, wait for the next response - * If the response is `OpenError.AppNotFound` and there are NO pending responses, return `OpenError.AppNotFound` +Message exchange: ```mermaid sequenceDiagram @@ -1482,34 +1467,15 @@ sequenceDiagram participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C - DA ->>+ DAB: Open Chart - DAB ->>+ DB: Open Chart - DAB ->>+ DC: Open Chart - DB ->> DB: App NOT Found - DB -->>- DAB: OpenError.AppNotFound - DC ->> DC: App Found - DC ->> DC: Open App - DC -->>- DAB: Return App Data - DAB -->>- DA: Augmented App Data + DA ->>+ DAB: open + DAB ->>+ DB: open + DB -->>- DAB: openResponse + DAB -->>- DA: openResponse ``` -__When the target Desktop Agent is set__ - -```mermaid -sequenceDiagram - participant DA as Desktop Agent A - participant DAB as Desktop Agent Bridge - participant DB as Desktop Agent B - participant DC as Desktop Agent C - DA ->>+ DAB: Open App - DAB ->>+ DC: Open App - DC ->> DC: Desktop agent in list and App Found - DC ->>x DC: Open App - DC -->>- DAB: Return App Data - DAB -->>- DA: Return App Data -``` +#### Request format -It sends an outward message to the bridge: +Outward message to the bridge: ```JSON // agent-A -> DAB @@ -1533,7 +1499,7 @@ It sends an outward message to the bridge: } ``` -which is repeated as: +which is repeated on to the target agent as: ```JSON // DAB -> agent-B @@ -1558,26 +1524,224 @@ which is repeated as: } ``` +#### Response format + +Response message from target Desktop Agent: +```JSON +// agent-B -> DAB +{ + "type": "openResponse", + "payload": { + "appIdentifier": { + "appId": "myApp", + "instanceId": "e36d43e1-4fd3-447a-a227-38ec48a92706" + } + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-..." + } +} +``` + +which is augmented and repeated on by the bridge as: + +```JSON +// agent-B -> DAB +{ + "type": "openResponse", + "payload": { + "appIdentifier": { + "appId": "myApp", + "instanceId": "e36d43e1-4fd3-447a-a227-38ec48a92706" + } + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-...", + "sources": [{ "desktopAgent": "agent-B" }] // added by DAB + } +} +``` + + +//TODO: further work required on the below messsage exchanges: ### findInstances -```typescript -findInstances(app: TargetApp): Promise>; +Type: **Request Response (collated)** or **Request Response (single)** + +Generated by API call: + +* [`findInstances(app: AppIdentifier)`](../api/ref/DesktopAgent#findinstances) + +e.g. + +```javascript +// Retrieve a list of all instances of an application +let instances = await fdc3.findInstances({appId: "MyAppId"}); + +// Retrieve a list of instances of an application on a specified Desktop Agent +let instances = await fdc3.findInstances({appId: "MyAppId", desktopAgent: "agent-A"}); ``` +Message exchange: + ```mermaid sequenceDiagram participant DA as Desktop Agent A participant DAB as Desktop Agent Bridge participant DB as Desktop Agent B participant DC as Desktop Agent C - DA ->>+ DAB: Find Instances of App. - DAB ->>+ DB: Find Instances of App - DAB ->>+ DC: Find Instances of App - DC --x DC: No Instance found - DB ->> DB: App Instance found - DB -->>- DA: Return App Data + DA ->>+ DAB: findInstances + DAB ->>+ DB: findInstances + DAB ->>+ DC: findInstances + DB ->> DAB: findInstancesResponse (B) + DC ->> DAB: findInstancesResponse (C) + DAB -->>- DA: findInstancesResponse (B + C) +``` + +#### Request format + +Outward message to the bridge: + +```JSON +// agent-A -> DAB +{ + "type": "findInstances", + "payload": { + "app": { + "appId": "myApp" + } + }, + "meta": { + "requestGuid": "", + "timestamp": "2020-03-...", + "source": { + "appId": "AChatApp", + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6" + } + } +} +``` + +which is repeated on to the target agent as: + +```JSON +// DAB -> agent-B +{ + "type": "open", + "payload": { + "app": { + "appId": "myApp", + "desktopAgent":"DesktopAgentB" + }, + "context": {/*contextObj*/} + }, + "meta": { + "requestGuid": "", + "timestamp": 2020-03-..., + "source": { + "appId": "AChatApp", + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6", + "desktopAgent": "agent-A" //added by DAB + } + } +} +``` + +If results should be constrained to a particular Desktop Agent, then set a `desktopAgent` field in `payload.app` and a matching `destination` field in `meta`: + +```JSON +// agent-A -> DAB +{ + "type": "findInstances", + "payload": { + "app": { + "appId": "myApp" + "desktopAgent": "agent-B" // destination agent + } + }, + "meta": { + "requestGuid": "", + "timestamp": "2020-03-...", + "destination": { "desktopAgent": "agent-B"}, //destination agent + "source": { + "appId": "AChatApp", + "instanceId": "02e575aa-4c3a-4b66-acad-155073be21f6" + } + } +} +``` + +The Desktop Agent Bridge should only forward the request to the requested Destkop Agent and handle the message exchange as a **Request Response (single)**. + +#### Response format + +Response message from a Desktop Agent: + +```JSON +// agent-B -> DAB +{ + "type": "findInstancesResponse", + "payload": { + "appIdentifiers": [ + { "appId": "myApp", "instanceId": "4bf39be1-a25b-4ad5-8dbc-ce37b436a344"}, + { "appId": "myApp", "instanceId": "4f10abb7-4df4-4fc6-8813-bbf0dc1b393d"}, + ] + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-...", + } +} +``` + +The bridge receives and collates the responses, augmenting each appIdentifier with a `desktopAgent` field, producing the following collated response which it sends back to agent-A: + +```JSON +// DAB -> agent-A +{ + "type": "findIntentResponse", + "payload": { + "appIdentifiers": [ + { "appId": "myApp", "instanceId": "4bf39be1-a25b-4ad5-8dbc-ce37b436a344", "desktopAgent": "agent-B"}, + //"desktopAgent" added by DAB + { "appId": "myApp", "instanceId": "4f10abb7-4df4-4fc6-8813-bbf0dc1b393d", "desktopAgent": "agent-B" }, + { "appId": "myApp", "instanceId": "920b74f7-1fef-4076-adef-63b82bae0dd9", "desktopAgent": "agent-C" }, + ] + }, + "meta": { + "requestGuid": "", + "responseGuid": "", + "timestamp": "2020-03-...", + "sources": [ //added by DAB + { "desktopAgent": "agent-A" }, + { "desktopAgent": "agent-B" }, + ] + } +} ``` +:::note +In the event that an agent times out or returns an error, where others respond, its `DesktopAgentIdentifier` should be added to the `meta.errorSources element` instead of `meta.sources`. +::: + +Finally, agent-A combines the data received from the bridge, with its own local response to produce the response to the requesting application: + +```JSON +// DAB -> agent-A +[ + { "appId": "myApp", "instanceId": "4bf39be1-a25b-4ad5-8dbc-ce37b436a344", "desktopAgent": "agent-B"}, + { "appId": "myApp", "instanceId": "4f10abb7-4df4-4fc6-8813-bbf0dc1b393d", "desktopAgent": "agent-B" }, + { "appId": "myApp", "instanceId": "920b74f7-1fef-4076-adef-63b82bae0dd9", "desktopAgent": "agent-C" }, + { "appId": "myApp", "instanceId": "688dbd5e-21dc-4469-b8cf-4b6a606f9a27" } //local response +] +``` + + ### PrivateChannels `PrivateChannels` are intended to provide a private communication channel for applications. In order to do so, there are differences in how their broadcasts MUST be handled and a number of additional message exchanges MUST be supported in order to handle events that are used to manage the channel's lifecycle. From 6e8aa0963d377ddbaf34e39975441abe0d6b65d0 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 13:31:26 +0100 Subject: [PATCH 58/61] Update spec.md update TODO list --- docs/api-bridging/spec.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 54a86dc4f..2606ee141 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -14,7 +14,7 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Adjust pseudocode for bridge message handling for cases where we receive a timeout but already have partial responses to process. * Exempted PrivateChannels from some cases where bridge messages are not normally generated (but should be for PrivateChannels). * Classified message exchanges by type. -* Completed message exchanges for broadcast, findIntent, findIntentsForContext and raiseIntent. +* Completed message exchanges for broadcast, findIntent, findIntentsForContext, raiseIntent, open & findInstances. * Added advice on whether other agents report to users on connect/disconnect events? (SHOULD) * Added/consolidated advice on handling error responses from Desktop Agents in Messaging protocol section @@ -26,9 +26,8 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope ## TODO list -* Complete message exchange documentation +* Complete message exchange documentation for PrivateChannels * Expand on how the DAB should create the JWT token (and its claims, which must change to avoid replay attacks) which it sends out in the `hello` message for DAs to validate. -* How to handle events from PrivateChannels (addContextListener, listener.unsubscribe, disconnect) * To create final PR: * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction * Add new errors to Error enumerations specified in this proposal From fe2b095456a9e9fe6d93445aff3d5393fbd7f607 Mon Sep 17 00:00:00 2001 From: Tiago Pina Date: Tue, 25 Oct 2022 13:55:14 +0100 Subject: [PATCH 59/61] typos and linting --- docs/api-bridging/spec.md | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 2606ee141..62f76c552 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -333,22 +333,20 @@ This procedure is the same for both previously connected and connecting agents, After applying the `connectedAgentsUpdate` message, the newly connected Desktop Agent and other already connected agents are able to begin communicating through the bridge. -#### Atomicity and handling concurrent operations +#### Atomicity and handling concurrent operations Handling by the Desktop Agent of the synchronization message from the DAB in step 6 of the connection protocol should be atomic to prevent message overlap with `fdc3.broadcast`, `channel.broadcast`, `fdc3.addContextListener` or `channel.getCurrentContext`. I.e. the `connectedAgentsUpdate` message must be processed immediately on receipt by Desktop Agents and updates applied before any other messages are sent or responses processed. -Similarly, the Desktop Agent Bridge must process steps 3-6 of the connection protocol (receiving a`handshake` messages upto issuing the `connectedAgentsUpdate` messages to all participants) as a single atomic unit, allowing no overlap with the processing of other messages from connected agents (as they might modify the state information it is processing during those steps). +Similarly, the Desktop Agent Bridge must process steps 3-6 of the connection protocol (receiving a `handshake` messages up to issuing the `connectedAgentsUpdate` messages to all participants) as a single atomic unit, allowing no overlap with the processing of other messages from connected agents (as they might modify the state information it is processing during those steps). #### Notification to users of connection events -Desktop Agents SHOULD provide visual feedback to end users when they or other agents connect or disconnect from the Destkop Agent Bridge (i.e. whenever a `connectedAgentsUpdate` message is received). Doing so will ensure that the end user understands whether their apps and Desktop Agent can communicate with other apps running under other Desktop Agents, and can better attribute any issues with interoperability between them to the probable source. + +Desktop Agents SHOULD provide visual feedback to end users when they or other agents connect or disconnect from the Desktop Agent Bridge (i.e. whenever a `connectedAgentsUpdate` message is received). Doing so will ensure that the end user understands whether their apps and Desktop Agent can communicate with other apps running under other Desktop Agents, and can better attribute any issues with interoperability between them to the probable source. ### Step 7. Disconnects Although not part of the connection protocol, it should be noted that the `connectedAgentsUpdate` message sent in step 6 should also be sent whenever an agent disconnects from the bridge to update other agents. If any agents remain connected, then the `channelState` does not change and can be omitted. However, if the last agent disconnects the bridge SHOULD discard its internal `channelState`, instead of issuing the update. - - - ## Messaging Protocol In order for Desktop Agents to communicate with the Desktop Agent Bridge and thereby other Desktop Agents, a messaging protocol is required. FDC3 supports both 'fire and forget' interactions (such as the broadcast of context messages) and interactions with specific responses (such as raising intents and returning a resolution and optional result), both of which must be handled by that messaging protocol and message formats it defines, as described in this section. @@ -461,7 +459,7 @@ Response messages will be differentiated from requests by the presence of a `met * multiple values for responses that were collated by the bridge.*/ sources: [AppIdentifier | DesktopAgentIdentifier], /** Array of AppIdentifiers or DesktopAgentIdentifiers for responses that were not returned - * to the bridge before the timeout or because an error occured. + * to the bridge before the timeout or because an error occurred. * May be omitted if all sources responded. */ errorSources: [AppIdentifier | DesktopAgentIdentifier] } @@ -519,9 +517,10 @@ Response messages do not include a `destination` field. Instead, a Desktop Agent Further, the Desktop Agent Bridge should also inspect the `payload` of both request and response messages and ensure that any `AppIdentifier` objects have been augmented with the correct `desktopAgent` value for the app's host Desktop Agent (e.g. if returning responses to `findIntent`, ensure each `AppIntent.apps[]` entry includes the correct `desktopAgent` value). Further details of any such augmentation are provided in the description of each message exchange. ### Handling of Error Responses + The FDC3 Desktop Agent API specifies a number of error enumerations that define specific error strings that should be used as the `message` element of a JavaScript `Error` to be returned to the requesting application via a rejected promise. In the event that an Error must be returned by a Desktop Agent to the Desktop Agent Bridge, the message should be selected from the [Error enumeration](../api/ref/Errors) normally used by the corresponding FDC3 function (i.e. `OpenError` for `open` calls, `ResolveError` for `findIntent` and `raiseIntent` etc.). However, Desktop Agent Bridging does NOT require that an `Error` object is returned across the bridge as it cannot be fully recreated from its constituent fields in JavaScript. Rather, return only the specified message string in the `error` field of the `payload`, which should then be used to initialize a JavaScript `Error` on the receiving end. It is also advisable to output additional logging (in the Desktop Agent Bridge) indicating that the error was originally generated by a remote Desktop Agent and to provide the relevant details. -For example, a `raiseIntent` targetted at an app instane that no longer exists might generate the following response from the Desktop Agent: +For example, a `raiseIntent` targeted at an app instance that no longer exists might generate the following response from the Desktop Agent: ```JSON // e.g. agent-B -> DAB in response to a raiseIntent call @@ -538,9 +537,9 @@ For example, a `raiseIntent` targetted at an app instane that no longer exists m } ``` -For messages that target a specific agent, the Destkop Agent Bridge will augment the message with a `source` field and return it to the calling agent and the appl that made the original request. +For messages that target a specific agent, the Desktop Agent Bridge will augment the message with a `source` field and return it to the calling agent and the app that made the original request. -However, API calls that require a collated response from all agents where at least one agent returns a successful respose, will result in a successful response from the Desktop Agent Bridge (i.e. no `error` element should be included), with the agents returning errors listed in the `errorSources` array. This allows for successful exchanges on API calls such as `fdc3.raiseIntent` where some agents do not have options to return and would normally respond with (for example) `ResolveError.NoAppsFound`. +However, API calls that require a collated response from all agents where at least one agent returns a successful response, will result in a successful response from the Desktop Agent Bridge (i.e. no `error` element should be included), with the agents returning errors listed in the `errorSources` array. This allows for successful exchanges on API calls such as `fdc3.raiseIntent` where some agents do not have options to return and would normally respond with (for example) `ResolveError.NoAppsFound`. Finally, to facilitate easier debugging, errors specific to Desktop Agent Bridge are added to those enumerations, including: @@ -1290,7 +1289,7 @@ If the `raiseIntent` request were made locally, agent-B would deliver the intent } ``` -This is encoded and sent to the bridge (ommiting the `getResult()` function) as: +This is encoded and sent to the bridge (omitting the `getResult()` function) as: ```JSON // agent-B -> DAB @@ -1421,11 +1420,6 @@ If the `IntentHandler` returned `void` rather than an intent result `payload.int } ``` - - - - - ### open (on `fdc3`) Type: **Request Response (single)** @@ -1526,6 +1520,7 @@ which is repeated on to the target agent as: #### Response format Response message from target Desktop Agent: + ```JSON // agent-B -> DAB { @@ -1565,8 +1560,8 @@ which is augmented and repeated on by the bridge as: } ``` +//TODO: further work required on the below message exchanges: -//TODO: further work required on the below messsage exchanges: ### findInstances Type: **Request Response (collated)** or **Request Response (single)** @@ -1674,7 +1669,7 @@ If results should be constrained to a particular Desktop Agent, then set a `desk } ``` -The Desktop Agent Bridge should only forward the request to the requested Destkop Agent and handle the message exchange as a **Request Response (single)**. +The Desktop Agent Bridge should only forward the request to the requested Desktop Agent and handle the message exchange as a **Request Response (single)**. #### Response format @@ -1740,14 +1735,13 @@ Finally, agent-A combines the data received from the bridge, with its own local ] ``` - ### PrivateChannels `PrivateChannels` are intended to provide a private communication channel for applications. In order to do so, there are differences in how their broadcasts MUST be handled and a number of additional message exchanges MUST be supported in order to handle events that are used to manage the channel's lifecycle. When a `ContextListener` is added to a remote `PrivateChannel`, removed from it or the `disconnect` function is called a notifications must be sent to the application that owns the channel. The bridge will add in the source agent (agent-A) and forward the message to destination (agent-B). -Such notification messages should be addessed to the Desktop Agent that owns the channel, which will route it to the owning application and any other listeners. If any of those listeners are remote, the message should be repeated back to the bridge, once for each listener with the destination set as a full `AppIdentifier`. Both these messages and broadcast messages MUST NOT be repeated back to the application that generated them. The source information on repeated messages should be unmodified to ensure that the message is attributed to the original source. +Such notification messages should be addressed to the Desktop Agent that owns the channel, which will route it to the owning application and any other listeners. If any of those listeners are remote, the message should be repeated back to the bridge, once for each listener with the destination set as a full `AppIdentifier`. Both these messages and broadcast messages MUST NOT be repeated back to the application that generated them. The source information on repeated messages should be unmodified to ensure that the message is attributed to the original source. #### `broadcast` @@ -1780,7 +1774,7 @@ Hence, the broadcast message should be modified such that it includes a destinat } ``` -For a broadcast to a `PrivateChannel` owned by another agent the detination should just include the `DesktopAgentIdentifier`. The message should then be repeated (once for each listener) from that Desktop Agent to other remote listeners without modifying the source information (to ensure that the message is attributed to the original source), but with a destination set as a full `AppIdentifer` for each remote listener. +For a broadcast to a `PrivateChannel` owned by another agent the destination should just include the `DesktopAgentIdentifier`. The message should then be repeated (once for each listener) from that Desktop Agent to other remote listeners without modifying the source information (to ensure that the message is attributed to the original source), but with a destination set as a full `AppIdentifier` for each remote listener. #### `onAddContextListener` From 407a13d0ab36b34fb25bcaec767ba7542d55f985 Mon Sep 17 00:00:00 2001 From: Kris West Date: Tue, 25 Oct 2022 14:01:10 +0100 Subject: [PATCH 60/61] Apply suggestions from code review --- docs/api-bridging/spec.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index 62f76c552..e72689290 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -1560,7 +1560,6 @@ which is augmented and repeated on by the bridge as: } ``` -//TODO: further work required on the below message exchanges: ### findInstances @@ -1735,6 +1734,8 @@ Finally, agent-A combines the data received from the bridge, with its own local ] ``` +//TODO: further work required on the below message exchanges: + ### PrivateChannels `PrivateChannels` are intended to provide a private communication channel for applications. In order to do so, there are differences in how their broadcasts MUST be handled and a number of additional message exchanges MUST be supported in order to handle events that are used to manage the channel's lifecycle. From 3202af54c452315d5a40a78d5a45f13e79410ce3 Mon Sep 17 00:00:00 2001 From: Kris West Date: Wed, 26 Oct 2022 10:29:43 +0100 Subject: [PATCH 61/61] Update todo and open questions --- docs/api-bridging/spec.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-bridging/spec.md b/docs/api-bridging/spec.md index e72689290..9fe17ed45 100644 --- a/docs/api-bridging/spec.md +++ b/docs/api-bridging/spec.md @@ -29,9 +29,11 @@ In any Desktop Agent bridging scenario, it is expected that each DA is being ope * Complete message exchange documentation for PrivateChannels * Expand on how the DAB should create the JWT token (and its claims, which must change to avoid replay attacks) which it sends out in the `hello` message for DAs to validate. * To create final PR: + * Create JSON schema/generated Typescript types for message exchanges * Add new terms and acronyms to FDC3 glossary and ensure they are defined in this spec's introduction * Add new errors to Error enumerations specified in this proposal * Add RFC 4122 - https://datatracker.ietf.org/doc/html/rfc4122 to FDC3 references page + * Upgrade FDC3 website to docusaurus 2 (or export diagrams to PNGs to go in docusaurus v1 site) ## Implementing a Desktop Agent Bridge