LimaCharlie Extension Framework
An Extension is a standardized way to build functionality on top of LimaCharlie.
Extensions abstract away from the builder the following concerns:
- Keeping of which LC Orgs are subscribed to the Extension.
- Storing credentials for all the LC Orgs subscribed.
- Basic configuration storage.
This way, you can focus on the functionality that sets you apart.
Extensions are bits of code you maintain and host, meaning you maintain control of your Intellectual Property.
You can host your Extension anywhere you'd like, the only requirement is for it to:
- Be reachable from the internet over HTTPS
- Have a valid SSL certificate going up to a known Root CA.
We also recommend hosting your Extension somewhere with high availability like:
- Google Cloud Run
- Google Cloud Functions
- AWS Lambda
- AWS Elastic Container Service
Fundamentally, the protocol Extensions use is based over HTTP webhooks, which means they can be implemented as REST applications in a container, or even in serverless environments like Cloud Functions and Lambda.
The main lifecycle of an Extension looks like this:
- Build the Extension.
- Register the Extension in LimaCharlie.
- You will see heartbeat webhooks start to reach your Extension.
- Subscribe an Organization (or many) to the Extension.
- The Extension will receive a webhook indicating the subscription.
- From this point on, this Org can now interact with the Extension.
The best way to get started is from the Basic Example.
This example demonstrates the basic schaffolding necessary for the Extension and implements a single action ping.
The main mechanism where you'll implement functionality is within callbacks. These callbacks will receive several bits of data allowing you to do your thing:
- Config: this is a dictionary that is stored in LimaCharlie's Hive system. It allows you to support basic configurations for your Extension without having to provision and manage storage.
- Organization: most callbacks will receive and instance of a
limacharlie.Organizationwhich is the root of the LimaCharlie SDK. This SDK will be pre-authenticated for you for the org who's callback you're receiving. This means you never have to store credentials. - Idempotent Key: a simple key for each unique callback sent to your Extension, you can use this to deduplicate requests if they are ever retried.
- Request Data: data related to a specific Action you've registered for.
- ACL: for Requests, a platform-set view of what the initiator may access with respect to LimaCharlie resource ACLs (see below).
LimaCharlie resource ACLs restrict the content of a resource (a sensor, a hive record...) by tagging it acl:<scope>: only principals holding the scope <scope> may read it. Multiple acl: tags on one resource are ANDed.
To let an Extension honor those ACLs, LimaCharlie adds a signed, top-level acl block to every request envelope it sends (it is never inside data, which is user-controlled):
"request": {
"org": {...}, "action": "...", "data": {...}, "config": {...},
"acl": {
"scopes": ["hr", "finance"],
"global": false,
"enforce": true,
"source": "user"
}
}scopes: scope names the initiator holds (normalized, noacl:prefix).global: the initiator holdsaccess.globaland is allowed everywhere.enforce: the org has at least one enabled scope.falsemeans nothing is restricted and the Extension may skip every ACL check for this request.source: one ofuser,impersonated,dr,continuation,internal. Informational only.
In Go the block is exposed as params.ACL (a common.ACLView) on RequestCallbackParams:
params.ACL.Allows(tags []string) boolapplies the platform rule: true whenGlobal, true when!Enforce, otherwise everyacl:tag intagsmust name a held scope. A bareacl:or an unknown scope locks the resource; tags without theacl:prefix are ignored; comparison is case-insensitive with whitespace trimmed and comma-joined entries are split.params.ACL.Present() boolreports whether the envelope carried the block.
Fail-closed default: when the block is absent (for instance an older LimaCharlie extension manager), the view is Scopes: nil, Global: false, Enforce: true, Present: false. Allows then returns false for any resource carrying an acl: tag and true for untagged ones, so an Extension that enforces ACLs can never leak a restricted resource because the platform did not say what the initiator holds.
Allows only ever restricts: use it in addition to your normal permission checks, never to grant access the org permissions would not already allow. The Python SDK exposes the same thing as msg_request.acl (an lcextension.ACL with allows(tags) and present), passed as an optional fifth argument to request handlers that declare one.
If your Extension writes D&R rules into the organization, for example a rule that sends a sensor's reply back to one of your Actions or one that runs on a schedule, give those rules an acl_scopes list containing *, next to detect and respond:
detect:
...
respond:
- action: extension request
extension name: my-extension
extension action: process
extension request: {}
acl_scopes:
- '*'On a sensor restricted by a resource ACL, the platform refuses extension request, service request and start ai agent unless the rule's acl_scopes covers the sensor's scopes. Your Extension cannot know an organization's scope names, so it lists *, which stands for the scopes your Extension's API key is a member of at the moment the rule fires. The organization's administrator decides what your Extension can reach by adding its key to a scope, or removing it.
- Write these rules with the organization client your callbacks receive, so the rule's author is your Extension's key.
*is only accepted from an org API key. - The platform adds an
acl_scopes_authorfield to the stored rule. If you read your rules back and compare them with what you wrote, ignore that field. - Rules that only
report,task, tag, etc. need noacl_scopes.
In the Go simplified package:
- The recurring update rule that
RuleExtensionandLookupExtensioninstall lists*. One installed before this existed gets it on the next update. - The rules your
GetRulescallback returns are written as you supply them: addacl_scopesto the ones that use one of the three actions above.acl_scopes_authoris ignored when a stored rule is compared with yours, so such a rule is not rewritten on every update. - A platform that does not know
*yet refuses the rule with an error namingacl_scopes. The rule is then written withoutacl_scopes, with a warning, so your Extension keeps working as it did before (without reaching restricted sensors). No other failure is handled this way, in particular notACL_SCOPES_UNAVAILABLE, which is temporary.
The Python SDK has no helper that installs rules: a Python Extension that writes its own rules sets acl_scopes on them the same way.
There are two types of opaque bits of data your Extension can leverage:
- Config
- Request
Each of those can vary a lot from Extension to Extension, and they also require use interaction. This is where Schemas come in. You should specify the Schema of your Configs and your Requests. The LimaCharlie webapp will use the Request Schema to display an interactive UI for users to make requests. Similarly, the webapp will use the Config Schema to display an interactive UI to configure the Extension.
This section is a description of how Extensions work. If you're simply developing a new Extension, you likely don't need to read any of this. If you want to implement your own SDK for a different language on the other hand, this will explain how it works under the hood.
Since Extensions are built around receiving webhooks from LimaCharlie, let's start there.
Your Extension will receive a webhook from LimaCharlie based on multiple event.
Webhooks are defined starting at the Message structure.
... to be completed...