Plugin Overview

Plugin Overview

Plugins provide an API for Clace Starlark code to call out to external systems. Plugins are implemented in Go. Every plugin API calls needs to be in the approved list for it to be permitted. See security for an overview of the security model.

Each plugin is identified by a unique name, like store.in or exec.in. Plugins ending with .in are internal plugins, built into the Clace binary. Support for external plugins which are loaded dynamically is planned.

Plugin Usage

To use a plugin, load it using

app.star
load("http.in", "http")

This adds http to the namespace for the app. To make a call to the plugin, first add the permissions to the app config.

app.star
    permissions=[
        ace.permission("http.in", "get"),
        ace.permission("http.in", "post")
    ],

Run clace app approve /myapp to authorize the app to call the get and post methods on the http plugin.

In the app handler code, do

app.star
    ret = http.get(SERVICE_URL + "/api/challenge/" + challenge_id)
    if not ret:
        return ace.response(ret.error, "invalid_challenge_id", code=404)

At runtime, Clace will check if the get call is authorized. If so, the call to the plugin will be performed.

Response Handling

All plugin API calls return a plugin_response structure. The fields in this are

  • error The error message string, empty string if no error
  • error_code The error code integer, zero if no error
  • value The actual return value for the plugin API call. The datatype for this depends on the API, check the API documentation for details.

To check the error status of an API call:

  • Check boolean value for the return. If false, that indicates an error which can be returned.
  • If no error, get the value property and continue with processing

For example,

app.star
    ret = http.get("https://localhost:9999/test")
    if not ret:
        # error condition
        return ace.response(ret, "error_block")

    # success
    print(ret.value.json()) # ret.value is the return value. The http plugin response has a json() function

An alternate way to write the error check is

app.star
    ret = http.get("https://localhost:9999/test")
    if ret.error:
        # error condition
        return ace.response(ret, "error_block")

    # Success
    print(ret.value.json())

Automatic Error Handling

Clace supports automatic error handling, so that the handler functions do not have to check the error status of every plugin API call. The way this is implemented is such that if no explicit error handling is done, then the automatic error handling kicks in. If explicit error handling is done, then automatic error handling is not done. See bookmarks app for an example of how the automatic error handling can be used.

If the error_handler function is defined, then that is called with the error. The manual error checking works the same as mentioned above. But if no manual error checking is done, then the Clace platform will automatically call the error_handler function in case of an error. The error_handler could be defined as:

app.star
def error_handler(req, ret):
    if req.IsPartial:
        return ace.response(ret, "error", retarget="#error_div", reswap="innerHTML")
    else:
        return ace.response(ret, "error.go.html")

When no explicit error checks are done, the automatic error handling happens in these three cases:

  • Value Access When the response value is accessed
  • Next API call When the next plugin API call happens (to any plugin function)
  • Handler Return When the handler function returns

Value Access

If the handler code is

app.star
    ret = http.get("https://localhost:9999/test")
    print(ret.value.json())

If the get API had succeeded, then the value property access will work as usual. But if the get API had failed, then the value access will fail and the error_handler will be called with the original request and the error response.

Next API Call

If the value is not being accessed, then the next plugin call will raise the error. For example, if the handler code is

app.star
    store.begin()
    bookmark = store.select_one(table.bookmark, {"url": url}).value

The response of the begin API is not checked. When the next select_one API is called, if the previous begin had failed, the select_one API will raise the previous API call’s error, the select_one will not run.

Handler Return

If the handler code is

app.star
    def insert(req):
        store.begin()
        book = doc.bookmark("abc", [])
        store.insert(table.bookmark, book)
        store.commit()

Assume all the API calls had succeeded and then the commit fails. Since the value is not accessed and there is no plugin API call after the commit call, the Clace platform will raise the error after the handler completes since the commit had failed.

Overriding Automatic Error Handling

The automatic error handling is great for handling the unusual error scenarios. For the error scenarios which are common, like a database uniqueness check failure, the error handing can be done explicitly in the handler code. If the handler code is

app.star
ret = store.insert(table.bookmark, new_bookmark)
if ret.error:
    return ace.response(ret, "error.go.html")

The automatic error handling will not be invoked in this case since the ret.error is being checked. Checking the truth status of ret also will disable the automatic error handling:

app.star
ret = store.insert(table.bookmark, new_bookmark)
if not error:
    return ace.response(ret, "error.go.html")
ℹ️
Note: When developing a new app, first define the error_handler and test it for the partial and full page scenarios. All subsequent handler code does not need to handle errors unless specific handling is required. If no error_handler is defined, a generic error message screen is returned. If is recommended to define a custom error_handler.

Plugin Accounts

Some plugins like exec.in do not require any account information. Others like store.in need some account information. The account configuration for a plugin is loaded from the Clace config file clace.toml. For example, the default configuration for store.in is here, which contains:

clace.toml
[plugin."store.in"]
db_connection = "sqlite:$CL_HOME/clace_app.db"

Any application using the store.in plugin will by default use the $CL_HOME/clace_app.db sqlite database. To change the default account config used by apps, update clace.toml and restart the Clace server. For example, adding the below will overwrite the default store.in config for all apps.

clace.toml
[plugin."store.in"]
db_connection = "sqlite:/tmp/clace_app.db"

Account Linking

If specific account config is required for an app, then the app can be linked to a specific account config. First add a new account config by adding in clace.toml

clace.toml
[plugin."store.in#tmpaccount"]
db_connection = "sqlite:/tmp/clace_app.db"

For an app /myapp using store.in, run clace account link --promote /myapp store.in tmpaccount

This links the myapp app to use the tmpaccount account.

Named Account

In addition to using account linking, the plugin code itself can point to specific accounts. For example, if the app code has

app.star
load("http.in#google", "googlehttp")

then the app will use the http.in#google account config by default. This also can be overridden using account links, by running clace account link --promote /myapp http.in#google myaccount

This approach is useful if an app has to access multiple accounts for the same plugin. The account linking approach is recommended for normal scenarios.

Clace apps aim to be portable across installations, without requiring code changes. Using account config allows the app code to be independent of the installation specific account config.