Extending Jarela¶
This is the single integration guide for everything you can plug into,
hook on top of, or layer around Jarela. It covers eight extension surfaces.
Inside the running app, the agent-callable
describe_extension_surfaces tool returns the
same map this document is built around.
For the proposed boundary between shared core work and user-owned specialization, see the post-3.0 direction and ADR-0111. They define future-work criteria, not new runtime protections. External code still executes with the trust of its host; capability labels are not a sandbox.
Public surface contract. Plugin authors should rely only on the type exports listed in CONTRIBUTING.md → Public API surface. Anything else is internal and may change between minor versions.
Built-in skill maintenance (Unreleased)¶
The seven packaged playbooks under lib/skills/builtins/ now share
name/description frontmatter and explicit Input, Output and Validation
contracts. Browser Navigation, Configuration, Documentation, Integrations,
Operations remain tool-guided procedures. Memory Organization and Personal
Briefing now include portable scripts for deterministic substeps (below).
None grants permissions or automatically executes a complete workflow.
User repositories and built-in read-only/override behavior are unchanged.
All seven are inventoried in lib/agents/prompt-registry.ts. Changing a packaged
skill triggers review of its complete static prompt and every assembled system
variant, because its discovery metadata appears in the skill catalog. Use
npm run prompts:dump -- --changed for uncommitted changes or
npm run prompts:dump -- --since origin/main for a branch.
Focused checks:
npx vitest run lib/skills lib/agents/prompt-registry.test.ts lib/agents/prepare
Tests check discovery, contracts, named tools, JSON examples against real schemas, and Personal Briefing's six deterministic fixtures. They do not prove that a model follows the procedure or that a live account/provider works. Memory Organization also has synthetic cleanup-planning fixtures. Adding fictional success fixtures for other procedures would not establish workflow correctness. No general JavaScript runner or milestone completion is introduced.
Script-backed built-in steps¶
| Skill | Portable implementation | Jarela entry point |
|---|---|---|
| Personal Briefing | personal-briefing/scripts/validate.mjs validates bounded structure, reference membership and source gaps |
Existing read-only validate_personal_briefing tool imports the same function |
| Memory Organization | memory-organization/scripts/plan-cleanup.mjs computes expired active facts and exact duplicate candidates at explicit UTC time |
New read-only plan_memory_cleanup tool imports the same function |
Both bundles include scripts/run.mjs: an authorized external host can invoke
node <skill-root>/scripts/run.mjs, send one JSON input on stdin and close stdin.
No npm dependencies, Jarela-private imports, filesystem scans, network or
credentials are required. Input is bounded to 64 KiB of valid UTF-8. Stdout is
one JSON result; exit 0 means ok: true, exit 1 means field validation failed
(ok: false with issues) or a transport error (stderr, no result). Copy each
whole skill directory, not just its Markdown. Tests copy bundles outside the
app and compare standalone results with typed-tool results; this is portability
evidence under Node, not Claude Code or other-host certification.
Resolve the script relative to the loaded SKILL.md, never relative to the
workspace or a hard-coded Jarela checkout. For example, a host may install the
whole bundle under .github/skills/, .agents/skills/, .claude/skills/ or a
personal skill directory. Pass the resolved absolute script path as one Node
argument (quote it when using a shell), stream JSON to stdin and close stdin.
Read JSON stdout on both success and validation-failure exits. Tests exercise
both bundles' fixtures under these layouts, including paths with spaces and
metacharacters, from an unrelated working directory.
Outside Jarela, tool names in the playbooks refer to Jarela's APIs, not shell commands or universally available tools. Use the host's authorized retrieval and memory capabilities; if equivalent capabilities or record fields are missing, report a blocker. The portable scripts supply only validation/planning, not retrieval, persistent memory, host permissions or automatic tool discovery.
The memory tool accepts at most 100 projections from retrieved facts records:
key, kind, subject, content, status, expires_at, plus explicit UTC
now at the top level. It does not retrieve memory itself. Archived entries are
excluded; expiry at or before now is reported. Duplicate candidates match
trimmed kind/subject/content and the exact expiry string. Provenance, confidence,
tags and history may still differ, so review full records before a separately
authorized archival. No merging, writing, deletion or trust changes occur.
Jarela invokes only these fixed reviewed implementations in-process through normal tool permissions. It never imports scripts from a user repository or substitutes overridden bundle source into the tool. A user override changes instructions, not built-in executable code. The optional checked skill runner remains unavailable; do not use terminal execution to bypass that boundary. The tiny CLI adapters are intentionally self-contained in each bundle so either directory can be shared alone; the substantive algorithm has one implementation shared with its Jarela tool, not a second tool-specific copy.
Browser Navigation, Configuration, Documentation, Integrations and Operations retain their existing typed tools and model judgment: no wrapper scripts are added merely to duplicate browser actions, credential probes, settings writes or document retrieval. Neither converted skill is a fully automated workflow.
Memory cleanup execution candidate (Unreleased)¶
Memory Organization now includes an experimental execution.json adapter over
its existing scripts/run.mjs and scripts/plan-cleanup.mjs, with a 5,000 ms
declared timeout and 64 KiB output budget. It is the first checked-runner
candidate, not an installed runner: ordinary reads are unchanged, and opt-in
read_skill execution inspection still returns execution_available: false.
The adapter requires the exact projection fields above and rejects additional
fields, unlike the existing typed tool's normalization or the standalone CLI's
tolerant input. Nullable expiry uses bounded anyOf; the output declares separate
success and field-error shapes. Date validity, trimmed text limits, unique keys
and expiry comparisons remain the planner's semantic checks. A schema-valid
invocation can therefore return a checked ok: false result with exit 1; valid
JSON is not execution success.
Tests capture the bundle's exact bytes, copy those captured bytes into test-only
temporary directories, and run fresh Node processes without PATH or credentials
from a different working directory. They verify fixtures, source mutation
independence, paths with spaces/metacharacters, record-count boundaries and
structured failures. This is a local CLI/contract test, not production snapshot
staging, artifact consent, process-tree cleanup, workflow persistence or
cross-host acceptance. In Jarela, continue using plan_memory_cleanup.
Table of contents¶
- Adding an LLM provider (built-in)
- Adding an LLM provider (external
.cjsplugin) - Adding a built-in tool
- Adding a bridge module
- Adding an MCP server
- Adding a custom harness
- Adding an integration manifest
- Inspecting skill execution metadata (Unreleased)
- Sharing skill bundles (design target)
- Reading shared skill bundles (Unreleased)
- Branding the app
- Agent introspection tools
Sharing skill bundles (design target)¶
ADR-0112
targets the open Agent Skills format:
one directory containing SKILL.md and optional scripts/, references/ and
assets/, shareable with Claude Code and other compatible hosts without
rewriting its source. Jarela's execution.json is optional, host-specific
execution metadata, not a prerequisite for a portable instruction-only skill.
This is a proposed compatibility requirement, not delivered cross-host
certification. Script runtimes, tool names, permissions and host-specific
features can differ. Jarela-specific tools still require Jarela; sharing a
bundle does not grant execution rights or make every Claude feature work here.
The first checked-runner design covers dependency-free .mjs/.cjs only.
Existing manifest inspection remains inert and always reports execution
unavailable. The ADR defines source-preservation, relative-resource and
cross-host fixture checks required before stronger compatibility claims.
Reading shared skill bundles (Unreleased)¶
Jarela can read the standard YAML metadata and relative text resources of an
Agent Skills directory without converting
its source. Use read_skill with inspect_bundle: true for metadata diagnostics:
{ "id": "example-skill", "inspect_bundle": true }
The additional bundle object reports standard, extended, legacy or invalid;
full metadata, field issues and compatibility notices; and
execution_available: false. standard means the checked metadata fields
meet the supported format constraints, not that another host or runtime was
tested. YAML aliases/custom tags, non-string mapping keys, non-finite numbers,
frontmatter above 64 KiB, depth above 32 or more than 4,096 value nodes produce
explicit issues. Name, description and optional standard fields are checked;
unknown host fields are retained and reported as unsupported behavior with
extended status rather than claiming standard-only metadata. Unicode names
and character counts follow the reference validator's naming rules.
Discovery still uses a 200-character description preview; inspection returns
the complete standard description (up to 1,024 characters). Nested metadata
does not replace top-level discovery fields. Legacy heading-only and flat
Markdown remain readable. Default read_skill responses are unchanged;
format diagnostics are opt-in and do not rewrite or reject legacy instructions.
The specification and
reference validator
were reviewed on 2026-10-28; the reported checks are not a complete conformance
suite or proof of another host's behavior.
Read a referenced file with:
{ "id": "example-skill", "path": "references/REFERENCE.md" }
Call read_skill_resource with that object. It returns id, path, source,
content, UTF-8 bytes and execution_available: false. Paths use /
relative to the selected skill root, regardless of the app working directory.
The same mechanism reads text assets or script source, including files absent
from an execution manifest. Each read is limited to 1 MiB and rejects missing,
nonregular, linked, nonportable, invalid UTF-8 or NUL-containing binary files.
Flat Markdown has no resource directory. Repository override order is shared
with read_skill; a missing resource in the winning bundle is an error, not
permission to read a different repo's copy.
No manifest is needed for either operation. Files stay untouched; script source
is data, never imported or executed. allowed-tools does not grant Jarela
permissions, and compatibility is an author declaration, not a verified
environment. Host-specific controls such as Claude's context: fork are
preserved but not enforced. Binary assets, import/export packaging, automatic
runtime/tool adaptation and cross-host execution certification are not added.
Concurrent-edit checks are best-effort read consistency, not an immutable
execution snapshot or host-code sandbox. The checked runner remains proposed
in ADR-0112.
Inspecting skill execution metadata (Unreleased)¶
The experimental read_skill option inspect_execution: true reads the
selected skill's execution.json and reports its declared files without
importing code, running package hooks, installing dependencies or granting
execution authority. Omitting the option preserves the existing response.
Prompt-only and flat Markdown skills still work without a manifest.
{ "id": "example-skill", "inspect_execution": true }
The normal skill content is returned with an additional execution object:
status: "not_declared": the selected folder has no manifest, or the skill uses a flat Markdown file.status: "invalid":issuescontains field/file paths and explicit errors. Malformed metadata does not cause fallback to another repository.status: "inspected":manifest, declaredfileswith byte sizes andtotal_bytespassed this structural inspection.
Every status includes execution_available: false. An inspected manifest
is not an approved, installed, executable or trustworthy skill. There is no
script runner in this slice. Inspection is a current filesystem observation,
not an immutable revision, dependency/import audit, sandbox or authorization.
It cannot establish that files remain unchanged after inspection.
Example folder manifest (experimental; not a public plugin contract):
{
"schemaVersion": 1,
"entrypoint": "scripts/main.mjs",
"files": ["scripts/main.mjs"],
"inputSchema": {
"type": "object",
"properties": { "name": { "type": "string", "maxLength": 100 } },
"required": ["name"],
"additionalProperties": false
},
"outputSchema": { "type": "string" },
"timeoutMs": 60000,
"maxOutputBytes": 65536
}
Only explicit .mjs and .cjs entry points are recognized; the entry point
must appear in files. Paths use portable relative / separators; absolute,
traversal, backslash, Windows-device and case-colliding paths are rejected.
The inspected skill directory, manifest and declared paths cannot be symbolic
links; declared leaves must be regular files. This is not a race-free snapshot.
Bounds are 64 KiB of UTF-8 manifest text, 64 declared files, 1 MiB per declared file and 4 MiB across declared files. File totals exclude undeclared files and the manifest/instructions unless explicitly listed. JSON is bounded to 32 levels and 4,096 value nodes before recursive schema checking. Timeout metadata defaults to 60,000 ms (allowed 1,000-120,000); output metadata defaults to 64 KiB (allowed 1-256 KiB). Both must be integers; invalid values are rejected, not clamped. These limits validate declarations; no execution budgets run here.
Schemas use explicit type: object (with a properties map), array (with
items), string, number, integer, boolean or null. Supported optional fields
are unique declared required properties, boolean additionalProperties,
minItems/maxItems, minLength/maxLength, minimum/maximum, and nonempty
scalar enums for string/number/integer/boolean. Unreleased bounded anyOf allows
two to four alternative schemas, including nullable fields and explicit result
variants; a value must match at least one branch. Alternative nodes accept only
anyOf, not sibling constraints. The invocation root still requires an explicit
object schema. Existing depth/node bounds include every branch. Unknown fields,
$ref, patterns, other combinators and executable validators are rejected. Inspection validates schema
structure, not actual invocation/results or satisfiability of all constraints.
Internal invocation/result validation (Unreleased)¶
The internal execution protocol helpers now check actual JSON against this
schema subset without spawning a process, reading a bundle or granting consent.
They require a manifest from the strict parser; they are not public plugin APIs
or a callable execution tool. Existing read_skill inspection remains inert.
- Invocation transport is exactly one JSON object, at most 64 KiB of UTF-8 text;
results are exactly one JSON value within the manifest's
maxOutputBytes. Whitespace counts toward the byte limit. Reject excess rather than parsing clipped text. The future runner must bound raw stdout bytes while streaming and decode valid UTF-8 before calling the string-based validator. - Both values use the existing depth-32 / 4,096-value-node bounds, including undeclared properties. Non-finite parsed numbers are rejected.
- Check types, required own properties, enums, nested values, inclusive numeric
and item/length bounds without coercion, defaults, stripping or mutation.
String lengths count Unicode code points. Omitted
additionalPropertiespermits undeclared values, as does explicittrue;falserejects them. - Validation reports up to 20 path-specific issues without echoing invalid
values. Property paths are still caller data, not sanitized display text.
The manifest parser explicitly rejects the reserved
__proto__schema property name rather than silently dropping it in the underlying validator. A failedanyOfreports one issue at the original value path, not every rejected branch's diagnostics; rejected branches do not pollute a later match. - Consent digest calculation reuses the bounded invocation-object parser and continues hashing exact original UTF-8 bytes, not a normalized serialization. A digest alone does not establish schema-valid input, a snapshot or consent.
This closes only the pure validation portion of the proposed protocol. It does not verify exit status, runtime identity, snapshots, authorization, cancellation, process cleanup or cross-host execution. Execution remains unavailable.
Internal raw-output capture (Unreleased)¶
SkillExecutionOutput consumes raw Uint8Array stdout/stderr chunks without
starting a process or granting execution. It validates a private copy of the
manifest, preallocates bounded buffers, copies chunks and rejects the first byte
past the declared stdout ceiling or fixed 16 KiB stderr ceiling. Byte-sized
chunks cannot create an unbounded retained chunk list. Both streams require
valid UTF-8, including multibyte sequences split across chunks; malformed bytes
fail immediately and incomplete trailing sequences fail on finalization.
finish() checks one complete stdout JSON result with the shared schema
validator and returns the checked value, bounded diagnostics and exact byte
counts. It does not strip a JSON BOM, repair text or parse clipped output.
Limit, encoding and result failures have explicit error codes and never return
an output value; the first failure remains terminal. Buffers are cleared and
released after failure, finalization or explicit disposal; later writes fail.
Clearing buffers is not a guarantee of erasing all runtime/OS copies.
This is protocol capture only. A JSON ok: false field-error envelope can pass
its declared schema, and even a valid success-shaped result does not prove a
zero process exit or verified cleanup. The future process owner must enforce
abort/timeout/exit/pipe completion and whole-tree termination before consuming
results, dispose on every exit path, and treat capture failures as a request
to stop the owned process. This helper neither terminates nor observes processes.
Diagnostics are untrusted, potentially sensitive script output; the helper
does not sanitize them or authorize display/persistence.
Checked execution and durable workflows (Unreleased)¶
ADR-0114 integrates the previously internal ADR-0113 engine. Discovery and resource reading remain inert. Explicit execution is available only when the host can establish process ownership; Security settings report the current availability and reason when unavailable.
Enable run_skill for the agent, initialize a workspace, then supply the skill
ID and exact input_json. Alternatively use Skills > Run a checked skill.
Default policy creates an execute_skill pending action. The shared review
banner shows exact invocation bytes, captured files and hashes, workspace,
runtime, limits and a full-trust acknowledgement. Approval executes once;
pending is never success. check_proposal, check_skill_execution and the
Skills controls expose the checked result; cancellation does not roll back
side effects. Approval requests expire after five minutes and cannot survive
restart. Captured code/arguments stay in memory; persisted checked script
results are encrypted. Exact trusted-revision receipts may waive repeat
invocation consent within their existing scope.
lib/stores/skill-run-staging.tscreates unique run-owned directories underJARELA_DB_DIR/skill-runs, writes captured bytes exclusively and verifies content, directory identity and absence of unexpected entries before launch. The writable workspace is separate and its canonical filesystem identity is checked twice. Mode bits do not provide same-user isolation or Windows ACL guarantees. A hostile same-user path race is outside this contract.lib/skills/execution-runner.tsorchestrates strict input validation, exact revision/input/runtime/limit binding, live policy rechecks, one-use consent consumption and directprocess.execPath/argv. The platform owner supplies only required OS runtime environment, not app credentials or PATH. Windows uses suspended launch and Job Objects with kill-on-close and positive job-drain checks. Linux requires a delegated writable cgroup-v2 subtree withcgroup.kill; missing delegation fails explicitly. macOS does not currently have a supported owner backend. Neither backend is a sandbox or permission boundary against arbitrary same-user host access.- The runner bounds execution and cleanup waits, withholds results after failed exit, cancellation, timeout, protocol failure or unverified cleanup, and disposes staging only after verified process cleanup. Unverified cleanup retains staging and reports failure; restart never executes leftover files. Startup and the local cleanup endpoint remove only recognized dead-owner prepared/positively-cleaned directories. Launched leftovers without cleanup evidence, changed identities and possibly live owners are retained with a reason. Nothing executes or automatically retries surviving files.
lib/workflows/definition.tsparses inert strict version-1 JSON, limited to 32 sequential steps and 64 KiB. Tool/script/validate/choice steps each declare input/output schemas and either literal input, separately schema-checked run invocation data, or a backward-only checked output/path binding. No expression language, dynamic imports or graph loops are evaluated. Required inputs remain objects.lib/workflows/runtime.tschecks every binding and output before downstream consumption. Its sole retry coordinator permits at most three attempts for declared read operations and six retries per run. Script launches, choices and writes do not automatically retry. The trusted host must independently verify operation replay classification and permissions; declarations cannot make full-trust script code read-only.lib/stores/workflow-runs.tsis an application SQLite store with encrypted state, versioned compare-and-swap updates and 512 KiB plaintext state bounds. Choices persist with exact run/revision/step/version checks, but answers are never script consent. Interrupted writes become uncertain and require validated observed reconciliation, not replay. Recovery of an interrupted owner requires a OS proof that the old process owner is dead; ambiguous/PID-reused owners remain blocked. Model-generated adapter outputs retain their label; schema validity is not factual verification.
Use Skills > Durable workflows or run_workflow with a versioned definition
and invocation. inspect_workflow, list_workflows, advance_workflow,
cancel_workflow and reconcile_workflow operate only on that agent's runs.
Each concrete tool also needs its own permission. The host checks declared read
effects against actual registered capabilities and refuses coordinator/proxy
recursion. Built-in workspace-aware tools receive the run's scoped workspace
per invocation, without mutating the agent's shared thread workspace; this also
holds after restart and during read-back. Script workflows first request a separate exact-plan approval;
this is deliberately required even if revision trust could authorize an isolated
invocation. The plan binds run, definition, invocation, workspace and captured
script revisions, permits each script step once, expires after five minutes
and is never restored after restart. Approval waiting does not consume a script
step's execution timeout. Resume explicitly after approval.
Human typed answers are submitted through local workflow controls/API, not an
agent answer tool. They bind run/revision/step/current version and do not grant
execution permission. A write may declare reconcileAction naming a permitted
read-back tool, called with the original input. Its observed JSON must satisfy
the write output contract. Missing observation support is an explicit blocker;
caller-provided guesses never mark a write reconciled.
The coordinator retains the actual host promise across timeout/cancellation
and blocks reconciliation until it settles. Late success or failure only
releases that guard; it never completes the uncertain step. Ownership is
shared process-wide by run ID, including across service/database instances.
Shutdown waits for these promises, not merely the response race.
Concrete tool invocations use the internal await_tool_settlement context
flag: the workflow owns the response timeout, bypassing wallclock/background
receipts. The tool runtime supplies a separate non-aborted SDK signal in this
mode, isolating both explicit and inherited caller signals: the SDK otherwise
rejects before a non-cooperative tool settles.
Tool-local timeouts still apply; workflow cancellation does not guarantee
remote rollback or termination. Script adapters retain their owned-process
cancellation/cleanup contract.
Local unlocked endpoints are /api/v1/skill-executions (request),
/skill-executions/[id] (inspect/cancel), /skill-executions/cleanup,
/api/v1/workflows (create/list) and /workflows/[id] (inspect/versioned
advance/answer/cancel/reconcile). Workflow definitions and invocation bytes are
schema-checked before persistence. API exposure does not grant remote callers
script approval authority. Tests cover real Windows descendants, app/broker
death and early parent exit, in addition to contract, CAS, restart and synthetic
workflow checks. Host-specific acceptance and human walkthroughs remain
separate release evidence; unit totals do not close the milestone.
The Memory Organization bundle also includes an inert workflow.json example:
supplied fact projections flow into the existing planner, then a typed keep/review
choice. It never archives or deletes. Its invocation and planner schemas are
checked against execution.json in tests to detect drift. Sharing/reading this
definition neither runs it nor authorizes its script step. The workflow definition
has its own content revision, separate from the script artifact/consent revision.
Inert authoring and local contribution bundles (Unreleased)¶
These unreleased W3/W4 workflows separate inert text builders from explicitly
approved lifecycle effects. The existing app manifest/release number does
not certify roadmap acceptance. Load the extension-development or
contribution-review built-in skill for the applicable procedure.
scaffold_extension is a Config/read tool. Supply a portable lowercase tool
name, scoped goal and discovery decision/evidence. Reuse/configure decisions
explicitly block authoring. An author decision returns a revision, suggested
extension-drafts/<name> relative staging directory and three text files:
the dependency-free CommonJS tool, Node fixture test and setup/recovery README.
This is a fixed local synthetic-loopback-service template, not implementation
of an arbitrary requested capability. Tests validate actual HTTP and checked
input/output, including missing configuration, invalid origin, wrong output,
oversized response and invalid UTF-8. Runtime generation never runs those tests.
The discovery claim remains caller-supplied, not independently verified.
Before any separately authorized staging write, verify the selected absolute
directory is outside all configured tools/providers/skill repositories. The
relative suggestion is not proof of isolation. Drop-in tools execute top-level
code automatically when their configured directory is scanned: copying there
is activation, not inert storage. Never install the fixture test beside the tool.
Review full-trust effects and authorize tests, exact-source installation and
agent permission separately using existing supported surfaces. Missing
execution/install support is a blocker; the scaffold has no execution manifest
and cannot be passed to run_skill. Generated guidance does not grant consent.
prepare_contribution_bundle is also Config/read. It accepts a destination
owner/repository, topic branch, base revision, classification, summary,
rights/license review, reported worktree/tests and up to 32 selected file changes.
Each file supplies a portable relative path, and before/after text (null
for an absent file). Each side is limited to 16,384 characters; total canonical
JSON input is capped at 64 KiB. Only well-formed UTF-8 text without NUL is
supported; binary and empty-file creation/deletion are excluded.
The local result contains exact before/after text and a full-replacement unified
patch, sorted paths, SHA-256 revision and explicit remaining gates. Destination,
branch, base, test/rights reports and all content bind the revision. Git's
line-ending settings can change application results; verify the destination's
policy and actual resulting bytes. The tool does not read repository state,
run git, execute tests, use credentials or send network requests. Reports are
explicitly caller_supplied_not_verified; verify them with trusted tools before
using them downstream. A reported dirty/unknown tree, failed/unrun tests or
unknown rights blocks bundle output.
Common secret markers share the repository scanner's pure pattern list. Contribution checks additionally block common credential assignments, email addresses, contributor home paths, sensitive filenames and case-colliding paths. Unlike the commit scanner, inline suppression comments cannot bypass this check. These are conservative heuristics, not complete privacy, licensing or ownership verification. Review every outgoing byte; use synthetic fixtures and never include secrets, user memory, raw chats or private service data. A successful check still requires human privacy and exact-diff review.
Both text-builder tools return bounded text only and always report no execution/publication authority; scaffolds also report no installation authority. No writes, imports, activation, durable grants or remote publication are performed by those builders. Keep the local bundle on denial/cancellation or missing credentials. Commit, push and draft PR remain distinct exact-diff/destination approval gates through existing authorized repository tools; local execution trust cannot satisfy them. The lifecycle services below supply distinct approved effects. Second-author walkthroughs and separately authorized live remote publication remain acceptance gates for #829/#830.
Reviewed extension lifecycle¶
Load extension-development, or open Tools > Skills > Extension and contribution
workflows. start_extension_authoring captures the selected name, goal and
discovery decision in a canonical workspace. An optional source replaces only
the CommonJS tool implementation (maximum 16 KiB UTF-8); the synthetic
contract and fixture assertions are fixed. Syntax validation does not import it.
Built-in name collisions, malformed/sensitive source and overlapping configured
autoload/staging paths are blockers.
Use request_extension_stage for one operation at a time:
stage: review exact files and approve a retained inert copy under the existing data-directoryskill-runs/run-*path. It is not auto-loaded.test: independently approve full-trust code execution. The existing checked Node runner owns descendants, limits time/output and validates the exact fixture result. Its environment does not inherit app credentials. Current capability, safe-mode and skill-execution policy checks still apply; a policy requiring a workflow-wide grant is not bypassed by a per-stage request. Unsupported native ownership fails explicitly.install: only observed passing tests qualify. Review exact candidate and previous source, agent, destination and import privileges. Only the tool.cjsis copied into the existing configured tools directory (created only by this approved effect if absent). It is initially disabled for invocation. The loader can still execute top-level code on import; disabled is not inert.configure: review an explicitfixture_originrestricted tohttp://127.0.0.1:<port>without credentials, path, query or fragment.enable: separately approve global drop-in enablement and granting only this tool to the selected agent. Other agent settings are preserved atomically; changed permission baselines require fresh review.verify: approve the actual permitted installed tool call against the synthetic service and check its result and installed bytes. Catalog presence is not verification.
For an operator-owned synthetic fixture, explicitly start this dependency-free
loopback server in a separate terminal, configure http://127.0.0.1:8791, and stop
it with Ctrl+C afterward. An agent needs separate execution authority to start
it; approving extension tests does not grant that authority.
node -e "require('node:http').createServer((req,res)=>{res.setHeader('content-type','application/json');res.end(JSON.stringify({value:new URL(req.url,'http://127.0.0.1').searchParams.get('value')}))}).listen(8791,'127.0.0.1')"
Use inspect_extension_authoring for the session and inspect_authoring_operation
for a pending/completed effect. cancel_authoring_operation never rolls back
effects; underlying execution remains owned until settlement and late success
does not convert cancellation into verified success.
On failed tests/update/verification stop downstream work. Separately approve
disable, then remove; removal revokes the selected agent's grant, verifies source absence and invalidates the
loader cache. Disabling does not kill a call/import already in flight.
restore disables and selects the originally retained prior bytes as a new
candidate; it does not copy them into the loader directory. Stage, test,
install, enable and verify again. There is no automatic rollback.
Retained inert source survives restart and is never an execution grant.
discard deletes only the current, identity-checked staged directory after its
own review; installed code is unchanged. Earlier retained versions remain
user-owned and require explicit inspection/removal. Restart invalidates all
sessions, pending effects and executable approvals; inspect retained files and
start a fresh review rather than resuming consent. See
ADR-0115.
Local unlocked endpoints mirror the tools:
POST /api/v1/extension-authoring accepts
{"action":"create","agentId":"...","workspace":"...","input":{...}} or
{"action":"request","agentId":"...","input":{"id":"...","stage":"test"}}.
GET with id and agentId inspects a session.
GET /api/v1/authoring-operations/<id>?type=extension returns exact review;
DELETE cancels. Approval/denial uses the existing pending-action endpoints
and requires a separate local user decision. No model-supplied boolean approves.
Reviewed contribution lifecycle¶
Load contribution-review, or select Contribution in the same Skills panel.
Existing fixes do not depend on extension authoring. request_contribution_tests
requires a canonical clean Git root, exact current base commit, existing local
Node/Vitest installation, supported native process owner, selected proposed
UTF-8 files and test paths. No dependency installation happens implicitly.
Unknown rights, sensitive content/paths, dirty source worktrees, stale base or
changed selected bytes block progression. Rights/license text remains a human
attestation, not a legal determination.
Example request shape (replace every synthetic value with reviewed actual data):
{
"destination": "synthetic-owner/synthetic-repo",
"branch": "fix/reviewed-value",
"base_revision": "0123456789abcdef0123456789abcdef01234567",
"classification": "core",
"summary": "Verify the selected synthetic change",
"ownership": "user_owned",
"license_review": "Synthetic user-owned fixture authorized for contribution",
"commit_message": "fix(fixture): verify reviewed value",
"files": [
{"path":"src/value.mjs","content":"export const value = () => \"after\";\n"},
{"path":"src/value.test.mjs","content":"import { expect, test } from \"vitest\";\nimport { value } from \"./value.mjs\";\ntest(\"value\", () => expect(value()).toBe(\"after\"));\n"}
],
"test_files": ["src/value.test.mjs"]
}
classification is core|extension; ownership is
user_owned|licensed_for_contribution|unknown. Null file content means deletion.
At most 32 files, 16,384 characters each, and 16 distinct JS/TS test paths are
accepted. Binaries and empty-file creation/deletion are unsupported.
Creation returns a pending test proposal, never a successful bundle.
Approval creates a new isolated topic worktree under the source workspace's
ignored node_modules/.cache/jarela-contributions, leaving its selected source
and unrelated changes untouched. Review test source, hooks and execution
effects: dependencies, Git hooks and imported/configured code can execute with
full host privileges. Passing tests alone do not prove privacy or rights.
The local exact operation preview contains observed test hashes/status/cleanup,
repository evidence, the canonical bounded diff and selected files.
Stop with that local bundle unless publication was requested. Then request
request_contribution_commit, request_contribution_push, and
request_contribution_draft_pr individually. Each accepts the same
{"operation_id":"..."} and returns a fresh proposal and review revision.
Commit includes exact identity/message/hooks/diff; push uses an explicit
create-only topic refspec and existing local Git credentials; draft PR uses the
configured GitHub authentication. Missing credentials never authorize a new
destination or asking for secrets in chat. Hooks/checks are not bypassed.
The empty expected-value lease prevents updating an already existing remote
branch, including one created during approval. No main push, merge, package
publication or automatic post-review write exists. Git mutations, hooks,
transports and credential helpers run under native process ownership; inert
discovery disables executable helpers rather than probing credentials.
inspect_contribution_operation returns status metadata to the model; the
complete reviewed source/diff remains local. The shared operation endpoint with
type=contribution exposes full preview and cancellation. Denial/cancellation
never rolls back effects; ambiguous outcomes block automatic retries. Restart
does not recover grants. Surviving user-owned worktrees require inspection before
fresh consent. Live remote publication remains separately authorized acceptance;
local bare-remotes and fake HTTP fixtures do not certify a real destination.
POST /api/v1/contribution-authoring accepts
{"action":"create","agentId":"...","workspace":"...","input":{...}} or
{"action":"commit","agentId":"...","operation_id":"..."} (likewise push
and draft_pr). Creation/read operations do not approve tests or publication.
Internal in-memory artifact capture (Unreleased)¶
SkillExecutionArtifact.capture is an internal, opt-in content-capture helper,
not discovery, a runnable snapshot, a grant or a public plugin API. It reads
execution.json, SKILL.md and the manifest's declared files through the same
bounded regular-file reader used for skill resources. Declared assets may be
binary; user-facing text-resource reads still reject binary/invalid UTF-8 data.
It never imports scripts, follows hooks, writes staging files or launches code.
The capture retains private copies of exact bytes and exposes defensive copies and frozen file metadata. Its SHA-256 revision includes a domain/version marker and length-framed sorted paths and contents, including the original manifest and instructions. Equal contents at different locations have equal revisions; later source changes or caller mutations cannot change the captured bytes. Manifest formatting changes also change the revision. Unlisted files are not included or attested.
The existing limits apply: up to 64 declared files plus automatic metadata, 1 MiB per file, 64 KiB for the manifest, and 4 MiB total including metadata. Explicitly declared metadata is counted once; case aliases for reserved metadata names are rejected. Symbolic links and special files at/below the selected skill root are rejected. A second bounded read detects observed changes during capture, and root filesystem identity is rechecked. This is not an atomic filesystem snapshot or protection against a hostile same-user process racing path resolution. In particular, this does not establish the identity of ancestors above the root.
The eventual run lifecycle must bound retained captures, verify an authorized workspace and policy, bind human consent, and privately stage and reverify these bytes before any launch. No durable staging location, execution grant, restart recovery or process-cleanup guarantee is introduced by this helper.
See ADR-0112 for the remaining artifact-approval, immutable-snapshot, process-cleanup and execution design gates.
Adding an LLM provider (built-in)¶
In-tree provider adapters (Anthropic, OpenAI, Gemini, DeepSeek, GitHub
Copilot, …) live under lib/providers/<name>.ts and export a ModelProvider.
lib/providers/builtins.ts imports and registers those objects with
lib/providers/registry.ts, which derives provider names from each module.
The static imports keep modules visible to Next's server bundler.
Steps:
- Create
lib/providers/<name>.tsexporting aModelProviderfrom@circuitwall/jarela/lib/providers/types. Implement at leastchat; addinvokeandstreamInvokeif the provider supports tools, andembed/listModelsif it supports those. - Add the exported provider object to the
PROVIDER_MODULESlist inlib/providers/builtins.ts. No separate provider-name map is needed. - If the provider has known model context windows that the live API
doesn't surface, register them in
lib/providers/known-context-windows.ts. - Document the new provider in the README "Providers" section.
Reference contract: lib/providers/types.ts (the ModelProvider
interface and its companion event/message types). See
lib/providers/anthropic.ts for a
non-trivial worked example with tool calling, prompt caching, and
multi-modal input.
ADR: 0013.
Adding an LLM provider (external)¶
If you don't want to vendor your provider in the tree (closed-source
gateway, vendor-internal model catalogue, etc.), drop a CommonJS plugin
into ~/.jarela/providers/<name>.cjs. It's hot-loaded per call — no
rebuild, no restart.
Steps:
- Copy
lib/providers/template-external.cjs.exampleto~/.jarela/providers/<name>.cjs. - Implement the same
ModelProvidershape from@circuitwall/jarela/lib/providers/types. The plugin file justmodule.exports = { name, chat, invoke?, streamInvoke?, ... }. - Confirm with the agent: ask
list_providersto see your name appear, anddescribe_providerfor capability/model details.
Auth, retries, and error mapping are the plugin's responsibility.
Capability flags (vision, tools, streaming, json_mode,
web_search, audio, files) come from listModels() if you implement
it; otherwise the runtime infers from which methods are present.
To make a declared credential testable from Credentials, add an optional
credentialProbe descriptor. It supports HTTPS GET probes with bearer,
custom-header, or query-parameter auth; common 401/403 and 429 classification
is handled by Jarela. credentialField must name one of the plugin's declared
credential fields. Keep the endpoint read-only and do not put secrets in static
headers or the endpoint URL:
credentialProbe: {
endpoint: "https://api.example.com/v1/models",
credentialField: "api_key",
auth: { type: "bearer" },
responseDetails: { models: { path: "data", count: true } },
},
Probe descriptors must use HTTPS and are checked when the plugin is loaded.
credentialField must refer to a declared secret field.
Providers without a safe read-only auth endpoint can omit the descriptor; they
remain available, but the integration test route will not claim to verify them.
ADR: 0013.
Adding a built-in tool¶
Built-in tools register themselves at module load. Choose the narrowest domain
folder under lib/tools/ first. For example, a Gmail tool
belongs in lib/tools/communications/, a filesystem tool in
lib/tools/filesystem/, and a new uncategorized local tool in
lib/tools/general/. Adding one is two files: the implementation and its
colocated test.
- Create
lib/tools/<domain>/<name>.ts:import { tool } from "@langchain/core/tools"; import { z } from "zod"; import { registerLangChainPackage } from "../packages/langchain-package"; export const myTool = tool( async ({ foo }) => JSON.stringify({ result: foo.toUpperCase() }), { name: "my_tool", description: "What this does, when the agent should call it.", schema: z.object({ foo: z.string() }), }, ); registerLangChainPackage({ category: "Files", tools: { read: [myTool] }, }); -
Add
import "../<domain>/<name>";tolib/tools/runtime/builtins.ts, keeping imports grouped by domain. -
Add
<name>.test.tsbeside the implementation. Tests should import the implementation from the same folder; do not add a root-level forwarding file just to preserve an old internal path.
That's it — no central array, no parallel category map. The new tool is
visible in GET /api/v1/tools, callable by every agent (subject to its
tool policy), and surfaced through the list_tools agent tool.
Capability gating (registerLangChainPackage({ category, tools: { read | write | execute: [...] } })):
read— pure read of local or remote state. No side effects.write— mutates local or remote state.execute— runs arbitrary code (shell, sandboxed eval, …).
External (.cjs) and MCP tools default to execute until manifest-level
overrides land. See ADR-0038.
Declare the backing integration when the tool needs credentials the operator configures in the Integrations panel:
registerLangChainPackage({
category: "Mail",
integrationId: "gmail",
tools: { read: [myTool] },
});
integrationId defaults to auth.integrationId when the package uses the
auth bridge, so packages wired that way need nothing extra. Agents are denied
tools whose integration fails its probe (permission_reason:
"integration_unconfigured") or whose declared credentials_required keys
resolve in neither process.env nor the env-sync allowlist
(credentials_missing). The tool stays listed in GET /api/v1/tools either
way so the operator can still find it and set it up — see
ADR-0078.
Only basic tools are bound to the model each turn. Everything else an
agent may run is reached through the invoke_tool proxy, so a new tool does
not need to be in the agent's pinned list to be reachable — it needs a
description good enough to be found by a list_tools keyword search.
There is no tool index in the system prompt.
Description text matters. The text in description is what the LLM
sees. Multi-sentence is fine — tell it WHEN to call this tool, not just
WHAT it does. Look at lib/tools/system/integrations.ts
for the pattern.
Reference contract: lib/tools/runtime/types.ts
and lib/tools/runtime/registry.ts.
Worked example: lib/tools/runtime/template.ts.
Adding a bridge module¶
Bridge modules adapt a messaging channel to Jarela's shared inbound message, chat-routing, and lifecycle contracts. The runtime and editor consume module metadata for connection setup, pairing, chat discovery, lookup, event controls, and attachment guidance.
- Create
lib/bridges/modules/<kind>/and implement aBridgeAdapterin a platform-specific file. - Export a
BridgeModulefrom the module entrypoint. Providekind, labels and instructions,createAdapter, andremoveAuth. Declarepairing,chatDirectory,chatLookup,groupEventSubscriptions, and attachment guidance only when the adapter supports them. - Add the module to
BRIDGE_MODULESinlib/bridges/registry.ts. This is the one static registration point needed for Next's server bundler. The registry validates bridge kinds for creation and publishes serializable metadata atGET /api/v1/bridges/kinds. - Add adapter and registry tests. The common UI uses the metadata; only add UI/API capabilities when the channel needs a connection flow that the descriptor cannot express.
Bridge remote IDs are opaque channel identifiers. The current route store and
wire contract retain the legacy remote_jid field name for compatibility, so
adapters should place their native chat ID there without relying on WhatsApp
JID syntax in shared routing logic. For an OAuth or credential-entry flow,
extend the module capability contract rather than adding kind checks to the
runtime or editor.
Message content and attachments¶
Tools and providers exchange message content through ContentPart from
@circuitwall/jarela/lib/tools/types:
textcarries plain user-visible text.imageis the legacy inline base64 image form. New persisted paths should preferimage_ref.image_refpoints at a content-addressed file under~/.jarela/files/, served byGET /api/v1/files/[name]and materialized to provider bytes only when the active model call needs vision input.filecarries inline text file content. Keep this for small text-like files whose contents should enter the prompt.file_refpoints at a binary file under~/.jarela/files/. Use this for PDFs, audio, video, and other binary files so chat-run JSON stays small.
External tools that produce large binary artifacts should write them through
the file store and return a ref-shaped content part or a /api/v1/files/[name]
URL. Do not embed large base64 blobs in normal chat/run payloads.
Hot-loading a vanilla LangChain tool package¶
Any npm package that exports a class implementing
StructuredToolInterface (the LangChain.js convention used by
@langchain/community, @langchain/google-community, third-party
packages, …) can be loaded into Jarela without writing code. The
operator manages the install themselves; Jarela reads a JSON manifest,
dynamic-imports the package, calls the constructor, and registers the
returned tool under a Jarela category + capability.
- Pick a directory for installed packages. Default is
~/.jarela/packages/; override withJARELA_PACKAGES_DIR. - Inside that directory, run
npm init -yonce, thennpm installeach package you want (e.g.npm install @langchain/community). - Create
<packages-dir>/manifests/<name>.jsonper tool:Fields:{ "package": "@langchain/community/tools/tavily_search", "export": "TavilySearchResults", "category": "Web", "capability": "read", "args": { "maxResults": 5 }, "requiredEnv": ["TAVILY_API_KEY"] } package— npm module specifier (resolved from your packages dir'snode_modules).export— named export. Default"default".category— Jarela category ("Web","Mail", … — same vocabulary as built-in tools, seeregistry.ts).capability—"read"/"write"/"execute". Default"execute"(conservative).args— constructor arguments, passed as a single object.requiredEnv— optional. If any listed env var is unset, the manifest is skipped (with askippedreason) instead of erroring.- Either restart the server (the agent sees the tool on its next turn)
or hit
POST /api/v1/packages/reloadto re-scan without restart.
Inspect the loader state any time with GET /api/v1/packages, which
returns the resolved packagesDir, registered tool names, skipped
manifests (with reasons, e.g. requiredEnv unset), and per-manifest
errors. Both endpoints are thin wrappers around
lib/tools/packages/langchain-packages.ts.
Installing a package via API. POST /api/v1/packages/install
with { "spec": "<npm-spec>", "version": "<optional>" } runs
npm install inside $JARELA_PACKAGES_DIR and returns the
introspected StructuredTool exports it found. Packages from a
publisher in PACKAGE_PUBLISHER_ALLOWLIST (default @langchain/*,
@circuitwall/*, langchain; extend via the
JARELA_PACKAGE_ALLOWLIST env var, same pattern as
ENV_ALLOWLIST in lib/env/allowlist.ts)
install immediately. Anything else returns 202 with a pending
approval id; GET /api/v1/packages/install lists pending approvals,
POST /api/v1/packages/install/:id approves and runs, and
DELETE /api/v1/packages/install/:id denies.
Registering an installed package. Once the package is on disk, the manifest-CRUD endpoints create the file that the loader picks up:
GET /api/v1/packages/manifests— list current manifests.POST /api/v1/packages/manifestswith{ name, package, export?, category, capability?, args?, requiredEnv? }— write a new manifest and trigger a reload. 409 on duplicate name.GET|PUT|DELETE /api/v1/packages/manifests/:name— fetch, upsert (replace), or remove a single manifest.
Each mutating call triggers reloadLangChainPackages() so the tool
becomes live (or disappears) on the agent's next turn.
Trust model. A loaded package runs with full Node privilege in the
Jarela process, same as JARELA_TOOLS_DIR extensions. Only install
packages you would npm install into any of your own projects.
Auth. The constructor's args are the auth surface for this
release — for packages whose env vars do the work (Tavily, SerpAPI,
many community tools) just set the env var and add it to requiredEnv.
A future PR will add an Integrations-panel form for packages that need
operator-managed credentials.
Adding an MCP server¶
Jarela's tool pool merges MCP-server tools with built-ins. To register a server:
- Via the UI. App → Settings → MCP → Add server. Pick
stdio(subprocess) orhttp(remote SSE/HTTP), fill in the spec, save. - Programmatically. Call
upsertMcpServer(...)fromlib/stores/mcp-servers.tswith aMcpServerInput. Spec env vars and headers are encrypted at rest (ADR-0005). - From the registry picker. The MCP picker UI fetches the official
registry (registry.modelcontextprotocol.io). Variables (
${TOKEN}etc.) are substituted viaapplyVariablesfromlib/mcp/registry.ts.
After save, the agent sees the new tools in its pool on the next turn —
verify with the list_tools or list_mcp_servers tools. If a server's
last_error is non-null, fix the spec and the next request retries.
ADR: 0014.
MCP results can include both human-readable content and machine-readable
structuredContent. Jarela appends the structured value as a text block labelled
Structured result (JSON):, so agents can use exact fields even when the text is
only a summary. Original text and multimodal content remain unchanged; private
protocol _meta is not copied into that block. Existing result-size limits still
apply. Tools without structured output retain their previous result shape.
Server-requested interactive input (elicitation) remains disabled.
Contract checked against the MCP adapters 2.0 migration guide on 2026-10-25. Regression tests cover HTTP discovery/invocation, structured-only results, multimodal preservation, and exclusion of protocol metadata.
Adding a custom harness¶
A harness is the system-prompt scaffold an agent runs under: it composes
sections like "plan first," "list capabilities up front,"
"propose-config-change rules," etc. Built-in presets live in
lib/agents/harness/presets.ts.
To add a custom harness:
- From inside the app. Use the agent's
propose_config_changetool with kindupsert_harness. The user approves; the harness is stored in thememory_storetable under theapp-settingsnamespace. - By editing presets. For an in-tree built-in harness, add a new
entry to
presets.ts— section keys, default order, body text.
An agent binds to a harness by harness_id on its config row. Resolution
order is per-agent → global default → builtin:default (see
lib/agents/harness/resolve.ts).
ADR: 0036.
Adding an integration manifest¶
An integration manifest tells the agent how to walk a user through
setting up a new external service (Atlassian, Gmail, GitHub, …). The
agent uses list_integrations and get_integration_setup tools to
narrate the recipe and proposes the corresponding config changes.
Steps:
- Add a manifest under
lib/integrations/registry.tswith prerequisites, ordered steps, and troubleshooting hints. - Each step that triggers a config change names its
proposeskind (e.g.install_mcp,update_agent_tools,update_agent). - Each step that produces a verifiable side effect names its
verify.toolso the agent can confirm success. - The manifest schema is enforced by
scripts/check-integration-manifests.mjs(runs on lint).
ADR: 0010.
Credential tests for built-in integrations are declared next to their fields
in lib/stores/integrations.ts. A read-only
HTTPS endpoint can use probe metadata (credential field, bearer/header/query
auth, optional environment fallback, status overrides, and allowlisted response
summaries) without adding a probe function. Keep custom code in
lib/health/probes.ts for multi-step OAuth, local
CLI, or non-HTTP authentication flows.
Branding the app¶
Jarela is published as @circuitwall/jarela on npm and licensed Apache-2.0.
Brand overlays (internal forks, white-label deployments) override identity
through configuration and assets — no source edits, so there is no rename
diff to rebase on every upstream release.
Installed-package build (Unreleased)¶
The npm package includes a branded build command, so an overlay needs only the Jarela dependency, a config file, and local assets—not a Jarela source checkout or source edits:
For example:
{
"name": "Acme Assistant",
"systemName": "Acme",
"assistantName": "Acme Assistant",
"assistantIdentity": "You are {{assistantName}}, a helpful assistant for {{systemName}}.",
"assets": {
"logoLight": "./brand/logo-light.svg",
"logoDark": "./brand/logo-dark.svg",
"icon192": "./brand/icon-192.png",
"icon512": "./brand/icon-512.png"
}
}
Install the dependency in the overlay project, then build and start Jarela:
npm install @circuitwall/jarela
npx jarela build-branded --brand ./brand.json
npx jarela start
The command stages source shipped in the installed package outside
node_modules (where Next.js/webpack excludes packages), applies the config
and assets, builds a standalone app, and replaces that package installation's
standalone bundle. Stop Jarela before building. The package installation must
be writable; each installation can hold one brand. Editing the config/assets
or updating Jarela requires rerunning the build and restarting the app. No
brand file is read at runtime.
Relative paths in assets resolve from brand.json and must resolve to
regular local files within its directory. Absolute paths, URLs, and symlinks
escaping that directory are rejected. Supported asset keys are
logoLight, logoDark, faviconSvg, faviconIco, icon192, icon512,
icon192Maskable, icon512Maskable, icon192Light, icon512Light,
icon192MaskableLight, icon512MaskableLight, and appleTouchIcon. Each is
optional; omitted assets use the upstream defaults. Supported file types are
slot-specific to match the app's declared icon types: faviconSvg requires
SVG, faviconIco requires ICO, and all icon... and appleTouchIcon assets
require PNG. logoLight and logoDark accept SVG, ICO, JPEG, PNG, or WebP.
Assets are limited to 10 MB each. The builder copies them into the branded
static bundle.
name is required. systemName defaults to name; assistantName defaults
to <systemName> Assistant. assistantIdentity defaults to a friendly
assistant identity for that system. Identity and description strings may use
{{appName}}, {{systemName}}, and {{assistantName}}. The assistant name
is a first-run default only: it does not rename existing agents or overwrite
their saved identities. Other optional top-level keys are shortName,
description, issueUrl, accentColor, and accentHoverColor. The two
color fields accept 3- or 6-digit hex colors. Invalid JSON, unsupported
fields, unsafe paths, and missing assets fail the build with an explicit
error.
NEXT_PUBLIC_APP_* environment variables remain supported for custom
source builds. The branded command maps brand.json values to those same
build-time settings. The upstream app continues to show no attribution under
its own name; rebranded UI keeps the existing "Powered by Jarela" credit
and version display. Apache-2.0 requires preservation of applicable notices but
does not itself require that visible product credit. Jarela's name and logo
are trademarks, governed separately from the software license.
The browser extension is the exception: its build is a packaging step over static files, so it needs no Next build.
The app and browser extension use separate brand configs: this brand.json
is consumed by the app build, while the browser extension's brand.json is
used when packaging its static MV3 manifest and icons.
Web app: NEXT_PUBLIC_APP_*¶
The build-time env vars below are the shared web-app branding contract. The
client-safe getters live in
lib/env/app-config.ts, exported as the public
subpath @circuitwall/jarela/lib/env/app-config.
| Env var | Drives |
|---|---|
NEXT_PUBLIC_APP_NAME |
Page title, PWA name, notification titles |
NEXT_PUBLIC_APP_SHORT_NAME |
PWA short_name (defaults to the app name) |
NEXT_PUBLIC_APP_DESCRIPTION |
Meta description, PWA description |
NEXT_PUBLIC_SYSTEM_NAME |
System ownership wording in agent prompts (defaults to app name) |
NEXT_PUBLIC_APP_ASSISTANT_NAME |
Initial assistant name in first-run setup |
NEXT_PUBLIC_APP_ASSISTANT_IDENTITY |
Initial assistant identity in first-run setup |
NEXT_PUBLIC_APP_ISSUE_URL |
"Report a bug" link — your tracker |
NEXT_PUBLIC_APP_LOGO_LIGHT |
In-app wordmark on light surfaces |
NEXT_PUBLIC_APP_LOGO_DARK |
Wordmark on dark surfaces (defaults to the light one) |
NEXT_PUBLIC_APP_ACCENT_COLOR |
--color-accent (hex only) |
NEXT_PUBLIC_APP_ACCENT_HOVER_COLOR |
--color-accent-hover (defaults to accent −15% lightness) |
NEXT_PUBLIC_APP_FAVICON_SVG / _ICO |
Favicon |
NEXT_PUBLIC_APP_ICON_192 / _512 |
PWA icons (any purpose) |
NEXT_PUBLIC_APP_ICON_192_MASKABLE / _512_MASKABLE |
PWA icons (maskable purpose) |
NEXT_PUBLIC_APP_ICON_192_LIGHT / _512_LIGHT (+ _MASKABLE_LIGHT) |
Light-background icon variants |
NEXT_PUBLIC_APP_APPLE_TOUCH_ICON |
iOS home-screen icon |
For installed-package rebranding, use the assets object in brand.json;
the branded command translates each asset to the matching env var and copies
it into the generated static bundle.
The rebranded UI keeps its upstream credit by design. Apache-2.0's copyright/license notices are separate from that product UI treatment. The software license does not grant permission to imply that a rebranded distribution is an official Jarela release.
Build-time environment details:
- These values are inlined by Next at build time. Rebuild the package to apply a changed brand config.
- The accent color must be a hex literal — 6-digit (
#7c3aed) or 3-digit (#7c3). It is injected into a<style>block, so anything else is rejected and ignored.
Browser extension: brand.json + npm run build:extension¶
npm run build:extension -- --brand ./brand.json --out dist/my-extension
Unlike the web app, this works from the installed package too — the tarball
ships browser-extension/ and the build script, so an overlay consuming
@circuitwall/jarela as a plain dependency can brand the extension without a
checkout:
node node_modules/@circuitwall/jarela/scripts/build-extension.mjs \
--brand ./brand.json --out dist/my-extension
// brand.json — all keys optional
{
"name": "Acme Assistant",
"shortName": "Acme",
"description": "Browser companion for Acme Assistant: …",
"accentColor": "#7c3aed",
"logo": "./brand/mark.png" // toolbar icons are regenerated from this
}
This emits a ready-to-load extension folder with a templated manifest.json
(name, description, toolbar title, command description), a regenerated
lib/brand.mjs, and rebuilt icons. Run without --brand and the output
matches the in-tree Jarela extension (a test enforces this).
See browser-extension/README.md.
What you may not rebrand¶
Rebranded builds keep a small "Powered by Jarela" link — on the web boot
screen and the extension options page — pointing at the upstream repository.
UPSTREAM_NAME / UPSTREAM_URL are plain constants, not env vars, and
build-extension.mjs never templates them. The credit renders only once
you have actually renamed the app; the upstream build shows nothing.
This is separate from NEXT_PUBLIC_APP_ISSUE_URL, which is yours to redirect.
Upstream-facing machinery (update checks, tool-telemetry issue drafts) also
keeps pointing at CircuitWall/jarela.
Internal identifiers stay jarela* on both surfaces — DOM ids, chrome.storage
keys, CSS class/keyframe names, globalThis keys, DB tables. They are not
product names, and renaming them orphans stored config.
Beyond the accent color¶
If you need deeper visual changes than one accent color, the pattern is still
to consume the package's .next/standalone build and apply tree-level
mutations in your own pipeline. Blessing arbitrary CSS overrides would turn
internal class names into a public API.
Agent introspection tools¶
The agent can introspect every extension surface above without any out-of-band knowledge:
list_tools— every tool currently in the pool, with category, capability, source. Filter by category / capability / source.list_providers,describe_provider({ name })— registered LLM providers and their static capabilities + known-model context windows.list_mcp_servers— configured MCP servers with enabled state, last error, transport, and tool count.describe_extension_surfaces— the curated catalog (mirrors this document) with registration entrypoints and doc anchors.list_integrations,get_integration_setup({ id })— agent-led setup manifests (the highest-level extension recipe).
When the user asks "what can you do" or "how do I add an X," the agent should call these instead of describing the system from memory.