puter-deploy / src /backend /doc /howto_make_driver.md
gionuibk's picture
Upload folder using huggingface_hub
61d39e2 verified
# How to Make a Puter Driver
## What is a Driver?
A driver can be one of two things depending on what you're
talking about:
- a **driver interface** describes a general type of service
and what its parameters and result look like.
For example, `puter-chat-completion` is a driver interface
for AI Chat services, and it specifies that any service
on Puter for AI Chat needs a method called `complete` that
accepts a JSON parameter called `messages`.
- a **driver implementation** exists when a **Service** on
Puter implements a **trait** with the same name as a
driver interface.
## Part 1: Choose or Create a Driver Interface
Available driver interfaces exist at this location in the repo:
[/src/backend/src/services/drivers/interfaces.js](../src/services/drivers/interfaces.js).
When creating a new Puter driver implementation, you should check
this file to see if there's an appropriate interface. We're going
to make a driver that returns greeting strings, so we can use the
existing `hello-world` interface. If there wasn't an existing
interface, it would need to be created. Let's break down this
interface:
```javascript
'hello-world': {
description: 'A simple driver that returns a greeting.',
methods: {
greet: {
description: 'Returns a greeting.',
parameters: {
subject: {
type: 'string',
optional: true,
},
},
result: { type: 'string' },
}
}
},
```
The **description** describes what the interface is for. This
should be provided that both driver developers and users can
quickly identify what types of services should use it.
The **methods** object should have at least one entry, but it
may have more. The key of each entry is the name of a method;
in here we see `greet`. Each method also has a description,
a **parameters** object, and a **result** object.
The **parameters** object has an entry for each parameter that
may be passed to the method. Each entry is an object with a
`type` property specifying what values are allowed, and possibly
an `optional: true` entry.
All methods for Puter drivers use _named parameters_. There are no
positional parameters in Puter driver methods.
The **result** object specifies the type of the result. A service
called DriverService will use this to determine the response format
and headers of the response.
## Part 2: Create a Service
Creating a service is very easy, provided the service doesn't do
anything. Simply add a class to `src/backend/src/services` or into
the module of your choice (`src/backend/src/modules/<module name>`)
that looks like this:
```javascript
const BaseService = require('./BaseService')
// NOTE: the path specified ^ HERE might be different depending
// on the location of your file.
class PrankGreetService extends BaseService {
}
```
Notice I called the service "PrankGreet". This is a good service
name because you already know what the service is likely to
implement: this service generates a greeting, but it is a greeting
that intends to play a prank on whoever is beeing greeted.
Then, register the service into a module. If you put the service
under `src/backend/src/services`, then it goes in
[CoreModule](..//src/CoreModule.js) somewhere near the end of
the `install()` method. Otherwise, it will go in the `*Module.js`
file in the module where you placed your service.
The code to register the service is two lines of code that will
look something like this:
```javascript
const { PrankGreetServie } = require('./path/to/PrankGreetServie.js');
services.registerService('prank-greet', PrankGreetServie);
```
## Part 3: Verify that the Service is Registered
It's always a good idea to verify that the service is loaded
when starting Puter. Otherwise, you might spend time trying to
determine why your code doesn't work, when in fact it's not
running at all to begin with.
To do this, we'll add an `_init` handler to the service that
logs a message after a few seconds. We wait a few seconds so that
any log noise from boot won't bury our message.
```javascript
class PrankGreetService extends BaseService {
async _init () {
// Wait for 5 seconds
await new Promise(rslv => setTimeout(rslv), 5000);
// Display a log message
console.debug('Hello from PrankGreetService!');
}
}
```
## Part 4: Implement the Driver Interface in your Service
Now that it has been verified that the service is loaded, we can
start implementing the driver interface we chose eralier.
```javascript
class PrankGreetService extends BaseService {
async _init () {
// ... same as before
}
// Now we add this:
static IMPLEMENTS = {
['hello-world']: {
async greet ({ subject }) {
if ( subject ) {
return `Hello ${subject}, tell me about updog!`;
}
return `Hello, tell me about updog!`;
}
}
}
}
```
## Part 5: Test the Driver Implementation
We have now created the `prank-greet` implementation of `hello-world`.
Let's make a request in the browser to check it out. The example below
is a `fetch` call using `http://api.puter.localhost:4100` as the API
origin, which is the default when you're running Puter's backend locally.
Also, in this request I refer to `puter.authToken`. If you run this
snippet in the Dev Tools window of your browser from a tab with Puter
open (your local Puter, to be precise), this should contain the current
value for your auth token.
```javascript
await (await fetch("http://api.puter.localhost:4100/drivers/call", {
"headers": {
"Content-Type": "application/json",
"Authorization": `Bearer ${puter.authToken}`,
},
"body": JSON.stringify({
interface: 'hello-world',
service: 'prank-greet',
method: 'greet',
args: {
subject: 'World',
},
}),
"method": "POST",
})).json();
```
**You might see a permissions error!** Don't worry, this is expected;
in the next step we'll add the required permissions.
## Part 6: Permissions
In the previous step, you will only have gotten a successful response
if you're logged in as the `admin` user. If you're logged in as another
user you won't have access to the service's driver implementations be
default.
To grant permission for all users, update
[hardcoded-permissions.js](../src/data/hardcoded-permissions.js).
First, look for the constant `hardcoded_user_group_permissions`.
Whereever you see an entry for `service:hello-world:ii:hello-world`, add
the corresponding entry for your service, which will be called
```
service:prank-greet:ii:hello-world
```
To help you remember the permission string, its helpful to know that
`ii` in the string stands for "invoke interface". i.e. the scope of the
permission is under `service:prank-greet` (the `prank-greet` service)
and we want permission to invoke the interface `hello-world` on that
service.
You'll notice each entry in `hardcoded_user_group_permissions` has a value
determined by a call to the utility function `policy_perm(...)`. The policy
called `user.es` is a permissive policy for storage drivers, and we can
re-purpose it for our greeting implementor.
The policy of a permission determines behavior like rate limiting. This is
an advanced topic that is not covered in this guide.
If you want apps to be able to access the driver implementation without
explicit permission from a user, you will need to also register it in the
`default_implicit_user_app_permissions` constant. Additionally, you can
use the `implicit_user_app_permissions` constant to grant implicit
permission to the builtin Puter apps only.
Permissions to implementations on services can also be granted at runtime
to a user or group of users using the permissions API. This is beyond the
scope of this guide.
## Part 7: Verify Successful Response
If all went well, you should see the response in your console when you
try the request from Part 5. Try logging into a user other than `admin`
to verify permisison is granted.
```json
"Hello World, tell me about updog!"
```
## Part 8: Next Steps
- [Access Configuration](./services/config.md)
- [Output Logs](./services/log.md)
- [Add HTTP Routes](./services/http.md)