-
Notifications
You must be signed in to change notification settings - Fork 2k
src backend howto_make_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 calledcomplete
that accepts a JSON parameter calledmessages
. - a driver implementation exists when a Service on Puter implements a trait with the same name as a driver interface.
Available driver interfaces exist at this location in the repo: /src/backend/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:
'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.
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:
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 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:
const { PrankGreetServie } = require('./path/to/PrankGreetServie.js');
services.registerService('prank-greet', PrankGreetServie);
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.
class PrankGreetService extends BaseService {
async _init () {
// Wait for 5 seconds
await new Promise(rslv => setTimeout(rslv), 5000);
// Display a log message
this.log.noticeme('Hello from PrankGreetService!');
}
}
Typically you'll use this.log.info('some message')
in your logs
as opposed to this.log.noticeme(...)
, but the noticeme
log
level is helpful when debugging.
Now that it has been verified that the service is loaded, we can start implementing the driver interface we chose eralier.
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!`;
}
}
}
}
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.
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.
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.
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.
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.
"Hello World, tell me about updog!"
This wiki is generated from the repository. Do not edit files the wiki.
You are reading documentation for Puter, an open-source high-level operating system.
Getting started with Puter on localhost is as simple as:
git clone https://github.com/HeyPuter/puter.git
npm install
npm run start