Administration and system
A plugin can look after the installation itself: the vault, accounts, plugins, backups, the project browsers, the machine's health. Requires apiVersion: 10. Every permission on this page is sensitive except docs:read and license:read: timon plugin add names them apart before you confirm the install, and a plugin that asks for one should say why.
All calls run with the rights of the account that makes the request, never more. Called outside a request (a background job), the plugin acts as itself and gets only what is said below. Errors carry a code: invalid, not_found, conflict, forbidden or unavailable (PluginAdminError); a refused caller gets a PluginCallerError, a permission missing from the manifest a PluginPermissionError. Every change made this way leaves a line in the audit log naming the plugin and the account.
| Permission | Surface |
|---|---|
secrets:list | ctx.secrets.list() — names, modes and grants, never a value |
secrets:manage | ctx.secrets.set / grant / revoke |
secrets:broker | ctx.secrets.httpCall() — an HTTP call under a brokered identity |
accounts:read, accounts:manage | ctx.users, ctx.roles, ctx.groups, ctx.backendAccounts |
plugins:read, plugins:manage | ctx.plugins.list / setEnabled, ctx.modules.list |
system:read, system:logs | ctx.system.health / doctor, ctx.system.logs |
backup:read, backup:manage | ctx.backup.status / enable |
browser:read, browser:manage | ctx.browser.list / status / start / stop |
agenda:read | ctx.agenda.list |
docs:read | ctx.docs.search / read |
license:read | ctx.license.edition() |
A :manage permission covers its :read.
Secrets
Nothing here returns a value. ctx.secrets.get (permission secrets:read) still only returns what was granted to your plugin.
const known = await ctx.secrets.list(); // account needs secrets:manage
await ctx.secrets.set({ name: 'CRM_TOKEN', value }); // creates; nobody can read it until a person grants it
await ctx.secrets.grant('CRM_TOKEN', 'agent:support'); // a superadmin's request only
const res = await ctx.secrets.httpCall({ identity: 'CRM_API', path: '/v1/contacts' });
listin a request shows the whole vault to an account that manages secrets. In a job it shows the secrets your plugin deposited and the ones granted to it.setcreates a name that does not exist yet, with no grant. It changes an existing secret only if your plugin created it, or if the request comes from a superadmin. A name held at several scopes answersconflict: use the Secrets page.grantandrevokeare a superadmin's, in a request: a job and a plain account are refused.scopeisproject:<slug>oragent:<name>. A plugin never grantsglobalnoragent:plugin:…, since either would let a plugin read a value it was not given.revoketakes any scope.httpCallacts as the agentplugin:<your id>: the identity must be a brokered secret granted to it (agent:plugin:<id>) or to everyone. The identity's own host allowlist applies, redirects are not followed, the value is masked in the answer and every call is journalled. Fields:identity,path, optionalmethod,host,headers,body. The answer is{ status, body, host }. A refusal hascodeforbidden/not_found/invalidand areason(no-grant,host-denied,unknown-identity…).
Accounts, roles and groups
A superadmin's request only — a call triggered by a signed webhook or an event, which runs as an account but never as a superadmin, is refused too. A job, an anonymous request and any other account — even one holding every right — are refused.
const users = await ctx.users.list();
await ctx.users.setStatus(id, 'disabled'); // revokes sessions and tokens
await ctx.users.setGroups(id, [groupId]);
const role = await ctx.roles.create({ name: 'Reader', permissions: ['kanban:read'] });
const group = await ctx.groups.create({ name: 'Support', roleIds: [role.id] });
await ctx.backendAccounts.grant(accountId, [userId]); // who may use a subscription account
What you do not get, on purpose: no credential ever comes back; no account can be created, invited, promoted or have its password reset; the status of a superadmin cannot be changed; and a plugin confers only ordinary rights of reading and use, from a short allowlist.
Rights a plugin may confer
A role or group a plugin writes may hold only: missions:spawn, missions:read, missions:stop, agents:communicate, trace:read, kanban:read, kanban:write, notify, storage, and the surfaces a person may see (page:/route, nav:slug, widget:id, theme:id, layout:id, each named, never with a wildcard), plus plugin:<your own id>, which opens your own package's API to the role (another plugin's plugin:<id> is refused). Everything else is refused with code: 'forbidden' and reason: 'right_not_grantable': projects:manage and agents:manage as much as *, terminal:open, host:exec, users:manage, plugins:manage, secrets:*, a :read:all scope, any wildcard such as kanban:*, and a deny (-right). A malformed right is invalid. The same rule covers a group: setGroups refuses a group, and groups.create / update a role, that holds anything outside the allowlist, even a role a person made.
Plugins and modules
const installed = await ctx.plugins.list(); // a request sees what its account may see; a job sees everything
const modules = await ctx.modules.list();
await ctx.plugins.setEnabled('@timon/agenda', false);
setEnabled needs the plugins:manage right on the account for a bundled module, and a superadmin for a third-party plugin. It is refused in a job and on your own plugin. Mind what that means: a plugin holding plugins:manage, in a request made by a superadmin, can switch off another third-party plugin, a monitoring plugin for instance. That is why plugins:manage is sensitive and why the operator reads it at install. Core modules cannot be switched off. Installing and removing plugins stays with the operator.
System, backup, browser, agenda, docs, license
ctx.system.health()— liveness, live missions, resource and memory figures.doctor()— the machine report (binaries, capabilities); a superadmin's request, or a job.logs({ kind: 'daemon' | 'desktop', lines })— the last lines, newest first, vault values masked, never the prompts log; a superadmin's request only, never a job.ctx.backup.status()andenable()(idempotent; there is nodisable).enableis a superadmin's request, or a job.ctx.browser.list / status / start / stop(project)— a request sees its account's sessions, a job the ones it started; another's session answers like a missing one. The debugging endpoint of the browser is never returned.ctx.agenda.list({ range, from, to, limit, … })— the connected Google calendars, read-only.ctx.docs.search(query)andctx.docs.read(route, heading?)— this version's documentation, offline.ctx.license.edition()—'lite'or'expert', and nothing else. Activating, installing or removing a licence is not available to plugins.