Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
143 KiB
Changelog
All notable changes to Armature are documented here.
v0.10.0 — Panel Manifest + Lifecycle
First version in the Panels series. Adds the plumbing layer for composable companion views — a new rendering tier between surfaces (full-page) and block renderers (inline). No presentation UI ships in this version; floating and docked chrome arrive in v0.10.1–v0.10.2.
Manifest: panels field (provider + consumer)
- Provider form: packages declare panels as a map of
{ entry, title, icon, description, min_width, min_height, default_width, default_height } - Consumer form: surfaces declare panel dependencies as an array of
"package.panel"strings (soft dependency — install succeeds even if provider is missing) - Validation enforces required
entryandtitlefor providers,pkg.paneldot-format for consumers
Backend: panel resolution at surface load
PanelMetastruct carries resolved panel metadataresolvePanels()resolves consumer references against installed and enabled provider packages at surface render timewindow.__PANELS__injected into base template for extension surfaces- Soft-dependency warning logged at install time for unresolved consumers
Frontend: sw.panels SDK module
sw.panels.open(panelId, opts)— lazy-loads panel JS via dynamicimport(), mounts into container, caches module for reusesw.panels.close(panelId)— calls cleanup, removes containersw.panels.toggle(panelId, opts)— open if closed, close if opensw.panels.isOpen(panelId)/sw.panels.isAvailable(panelId)— boolean queriessw.panels.list()/sw.panels.active()— registered and open panel IDs- Events:
panels.openedandpanels.closedemitted on lifecycle transitions - Mount context:
{ sw, params, panelId, close, resize } - Unstyled container (position:fixed div) — v0.10.1 adds FloatingPanel chrome
Tests: 10 new — 6 manifest validation, 4 panel resolution
v0.9.9 — Surface Access via Roles
Surfaces can now gate access by team role. A manifest declaring
"access": "role:approver" restricts the surface to users who hold the
approver role in any team — consistent with how group:NAME works.
New surface access level: role:ROLENAME
- Added to both
evaluateAccess()andvalidAccessLevels()validation - User must hold the specified role in at least one team (any-team semantics — no team context needed in the URL)
- System admins bypass role checks, consistent with
RequireRolemiddleware - Unauthenticated users get redirected to login
- Graceful fallback: if Teams store is nil, access is denied (fail closed)
New store method: HasRoleInAnyTeam(ctx, userID, role)
- Checks both primary role (
team_members) and additional roles (team_user_roles) across all teams in a single query - Implemented for both SQLite and PostgreSQL
Refactor: evaluateAccess promoted from package-level function to
Engine method to enable store access for role lookups.
Tests: 10 new tests — 5 handler integration tests (granted, denied, unauthenticated, admin bypass, nil store), 3 store tests (primary role, additional role, no match), 2 validation tests (valid role, empty role name)
v0.9.8 — Conditional Routing → SDK Primitive
Promotes the workflow branch-rule engine to a generic Starlark SDK module available to all extensions — no permission required.
New Starlark module: routing
routing.evaluate(rules, data)— evaluates an ordered list of condition rules against a data dict; returns the first matching rule'stargetstring, orNoneif no rule matches- 10 operators:
exists,not_exists,eq,neq,gt,lt,gte,lte,in,contains - First-match-wins semantics
- Domain-agnostic: uses
target(nottarget_stage) so any extension can use it for feature flags, content routing, approval logic, etc. - Always available — pure computation, no I/O, no permission gate
Tests: 8 new unit tests covering all operators, type coercion, first-match-wins, empty/missing/bad input
v0.9.7 — Full Read/Write Workflow Starlark Module
Extensions with workflow.access permission can now start, advance,
cancel, and signoff workflow instances directly from Starlark scripts.
WorkflowEngine interface
- Defined in
sandboxpackage to break theworkflow ↔ sandboxcircular import (followsNotificationSender/ConnectionResolverpattern) - 4 methods:
Start,Advance,Cancel,SubmitSignoff workflow.Enginesatisfies the interface implicitly — zero changes to the engine package
New Starlark builtins
workflow.start(workflow_id, data={})→ instance dictworkflow.advance(instance_id, data={})→ instance dictworkflow.cancel(instance_id)→ Noneworkflow.submit_signoff(instance_id, decision, comment="")→ signoff dict- All write ops require authenticated user context (
RunContext.UserID) - Graceful errors when engine or user context unavailable
Refactors
instanceToDicthelper extracted — shared byget_instance,start,advancesignoffToDicthelper for signoff return valuesdictToJSONhelper for Starlark dict →json.RawMessageconversion
Tests: 6 new unit tests (mock engine, happy path + error guards)
v0.9.6 — Deprecate stage_type, Collapse stage_mode
Simplifies the workflow stage classification model by removing redundant fields.
stage_type deprecated
- No longer validated on input; any value accepted, defaults to "simple"
- Existing manifests parsed for backward compatibility
- DB column retained; export no longer includes the field
starlark_hookpresence (notstage_type) determines automation
stage_mode collapsed (4 → 3 values)
- "review" removed as valid mode; mapped to "form" on input
- Existing DB rows migrated: review → form
- Review surface removed from workflow.html (~110 lines); signoff system
in
stage_config.validationhandles review behavior - Valid modes: form, delegated, automated
DB migration 018
- Postgres: UPDATE + CHECK constraint replacement
- SQLite: UPDATE only (CHECK stays broad)
Package manifests updated
- bug-report-triage, content-approval, employee-onboarding, webhook-notifier: review → form, stage_type removed
v0.9.5 — Typed Forms → SDK Primitive
Promotes the typed form system from a workflow-only model to a reusable SDK primitive. Any package can now declare and validate forms — not just workflow stages.
Backend — server/forms/ package
- Extracted
TypedFormTemplate,FormField,FormFieldset,FormOption,FormValidation,FieldCondition,FormHooks,FieldErrorfrommodels/workflow.gointo standaloneformspackage. ParseTypedFormTemplate(),ValidateFormData(),EvaluateFieldCondition()exported for use by any consumer.- ~317 lines removed from
models/workflow.go(no external imports existed).
REST endpoint
POST /api/v1/forms/validate— authenticated endpoint. Accepts{template, data}, returns{valid: bool, errors: [{key, message}]}.
Starlark module
forms.validate(template, data)— gated byforms.validatepermission. Returns dict withvalid(bool) anderrors(list of dicts).
Frontend SDK
sw.forms.render(container, template, opts)— Preact component rendering flat and progressive (fieldset) forms. Returns{getData, setErrors, destroy}.sw.forms.validate(template, data)— client-side validation (mirrors Go logic).sw.forms.validateRemote(template, data)— server-side validation via REST.- SDK version bumped to
0.9.5.
Manifest validation
form_templateaccepted at package level. Validated viaforms.ParseTypedFormTemplate()at install time.ManifestInfo.HasFormTemplateflag added.
Tests
- 16 new tests: 12 forms package unit tests (parse, validate by type, conditions, fieldsets), 4 Starlark module tests (valid, required, type errors, conditions).
v0.9.4 — Package Adoption + Roles
Packages can now declare adoptable: true in their manifest. When a team
adopts an adoptable package, a team-scoped copy is created that references
the original (shared assets, no disk duplication). The package's
requires_roles auto-populate into a new team_role_catalog table so
team admins know which roles to assign.
Schema
- Migration 017:
adoptableandadopted_fromcolumns onpackages.team_role_catalogtable (team_id, role, source_package_id) with unique constraint. Both Postgres and SQLite dialects.
Store + Models
PackageRegistration:Adoptable bool,AdoptedFrom *string.PackageStore:ListAdoptable(),GetByAdoptedFrom().TeamRoleCatalogEntrymodel struct.TeamStore:AddRoleToCatalog,ListRoleCatalog,RemoveRoleCatalogBySource.
Handlers
POST /teams/:teamId/packages/:id/adopt— adopt a global adoptable package into the team. Creates team-scoped registration, clones workflow if applicable, populates role catalog. Idempotent.GET /teams/:teamId/packages/adoptable— list available packages with adoption status per team.DELETE /teams/:teamId/packages/:id/unadopt— remove adopted package and clean up role catalog entries.GET /teams/:teamId/roles/catalog— list known roles for a team.
Manifest Validation
adoptable: trueparsed asManifestInfo.Adoptable.- Rejected on
libraryandtest-runnertypes. - Persisted to DB on package install.
Deprecation
POST /teams/:teamId/workflows/:id/adopt(AdoptTeamWorkflow) now returnsX-Deprecatedheader and logs a deprecation warning. Use the package-level adoption endpoint instead.
Tests
- 11 new tests: 5 manifest validation, 3 role catalog store, 3 handler integration (adopt success, idempotent, non-adoptable rejection).
v0.9.3 — Team User Roles
Promotes the team role system from a single-role-per-member model to a many-to-many relationship, enabling users to hold multiple roles within a team simultaneously.
Schema
- Migration 016:
team_user_rolestable (team_id, user_id, role) with unique constraint and compound index. Both Postgres and SQLite dialects.
Store + Models
TeamUserRolemodel struct.- 6 new
TeamStoremethods:AddUserRole,RemoveUserRole,ListUserRoles,GetMemberRoles(union of primary + additional),HasRole,RemoveAllUserRoles. - Implementations for both Postgres and SQLite stores.
Middleware
RequireRole(teams, roles, stores)— kernel middleware that checks whether the user holds at least one of the required roles (OR semantics). System admin bypass via permissions.
Handlers
GET /teams/:teamId/members/:memberId/roles— list full role set.POST /teams/:teamId/members/:memberId/roles— assign additional role.DELETE /teams/:teamId/members/:memberId/roles/:role— remove role.RemoveMemberhandler now cleans upteam_user_roleson member removal.
Manifest
requires_rolesfield parsed from package manifests (advisory in v0.9.3; extensions gate viateams.has_role()in Starlark).
Starlark SDK
- New
teamsmodule wired into sandbox runner:teams.get_member_roles(team_id, user_id)→ list of strings.teams.has_role(team_id, user_id, role)→ True/False.
Admin UI
- Team-admin members page: removable badge chips for additional roles, "+ Role" dropdown for assignment.
- Fixed pre-existing SDK auto-unwrap bug in
loadRoles/loadMemberRoles.
Tests
- 10 new tests: store CRUD (add, idempotent, has_role, remove, removeAll), middleware (allowed, denied), manifest parsing (valid, empty, invalid).
v0.9.2 — Starlark Converter Consolidation + Snapshot Cleanup
Consolidates duplicated Go↔Starlark conversion code and snapshot parsers into canonical locations, removing ~350 lines of copy-paste across 8 files.
Converter consolidation
- New
sandbox/convert.gowith four exported functions:GoToStarlark,StarlarkToGo,DictToMap,MapToDict. - Superset implementation handles all Go primitive types (nil, bool, int, int64, float64, string), containers (map, slice), and Starlark Tuple — covering every variant that previously existed.
- Deleted duplicate converters from
workflow/automated.go,handlers/starlark_helpers.go,handlers/workflow_hooks.go,sandbox/workflow_module.go,sandbox/realtime_module.go,sandbox/files_module.go,triggers/event.go,triggers/webhook.go. - SQL-specific converters in
db_module.go(error-returning,[]bytehandling) intentionally excluded — different semantics.
Snapshot parser consolidation
- New
models/snapshot.gowithParseSnapshotStages()handling both wrapped{"stages":[...]}and legacy flat[...]formats. - Deleted three identical parsers from
workflow/engine.go,handlers/workflow_instance_handlers.go, andhandlers/workflow_assignment_handlers.go.
Snapshot format standardization
workflow_packages.gopublish path now emits wrapped format, matchingworkflows.go. All snapshot creation is consistent.
Tests: 7 new converter tests with round-trip coverage. Existing engine, handler, and workflow tests updated and passing.
v0.9.1 — Server-Side Sub-Path Routing
Hardens multi-surface routing so full-page refreshes on sub-paths work correctly server-side, with early auth optimization and back-button resilience.
Route consolidation
- Root (
/s/:slug) and catch-all (/s/:slug/*path) handlers now share a singledispatchfunction. The root handler injectspath="/"and delegates, eliminating the separate code path.
Early auth short-circuit
- New
aggregateAccess()function scans all surfaces to determine whether a package is all-public, all-authenticated, or mixed. - All-authenticated packages redirect to login before surface matching,
skipping
matchSurface()+evaluateAccess()overhead. - Mixed-access packages fall through to per-surface checks as before.
SDK back-button fix
- On boot,
history.replaceState()seeds the initial history entry with__SURFACE_PATH__and__SURFACE_PARAMS__. This fixes back-button navigation fromsw.navigate()to the initial server-rendered view. - SDK version bumped to
0.9.1.
Tests: 13 new
- 5
aggregateAccessunit tests (all-public, all-auth, mixed, default-access, single-public). - 8 handler integration tests covering root refresh, sub-path static and param refresh, unauth redirect, public anonymous, mixed-access, non-matching sub-path, and early auth short-circuit.
Modified files:
server/pages/pages.go—aggregateAccess(), route consolidation, early auth short-circuit inRenderExtensionSurfaceserver/pages/pages_surface_match_test.go— 5 new unit testsserver/pages/pages_handler_test.go— 8 new integration testssrc/js/sw/sdk/index.js—history.replaceState()seed, version bump
v0.9.0 — Multi-Surface Packages
Packages can now declare multiple surfaces — each with its own path, access level, title, and layout. A single package can serve a public submission form, an authenticated dashboard, and an admin settings page. The kernel resolves incoming requests to the correct surface and enforces access server-side.
Manifest: surfaces array
- Packages declare a
surfacesarray with entries like{ "path": "/submit", "access": "public", "title": "Report a Bug" }. - Each surface has independent
path,access,title,layout, andnavfields. Package-levelauthandlayoutserve as defaults. - Supported access levels:
public,authenticated,admin,group:{name}. - Packages without
surfacesget auto-synthesis from legacyauth/layoutfields — no existing packages break.
Kernel: unified route tree
- Surface handler and ext API handler share a single
/s/:slugroute tree viaRegisterExtensionRoutes. The dispatcher checks the path prefix:/api/*→ ext API handler with JWT auth; everything else → surface handler with per-surface access checks. matchSurface()resolves request paths against surface patterns with Gin-style:paramsupport. Static segments preferred over params.evaluateAccess()checks access requirements per-surface, with login redirect for unauthenticated users and 403 for insufficient permissions.
Frontend: __SURFACE_PATH__ + sw.navigate()
window.__SURFACE_PATH__andwindow.__SURFACE_PARAMS__injected into every extension surface page. Packages use these to decide which view to render.sw.navigate(path, params)for SPA-style intra-package routing viapushState. Emitssurface.navigateevents. Handles back/forward viapopstate.- SDK version bumped to
0.9.0.
Navigation
extensionNavItemsreads thesurfacesarray to find the nav entry (firstnav: true, or root/).- Fix: packages with
status: pending_reviewno longer appear in the sidebar navigation.
Validation
ValidateManifestvalidatessurfacesentries: path required, must start with/, no duplicates, access level must be recognized.- Empty
surfacesarray rejected. Non-arraysurfacesrejected.
Tests: 22 new
- 11 manifest validation tests (surfaces valid/invalid, auto-synthesis, group access, duplicate paths).
- 11 route matching tests (static paths, param extraction, specificity ordering, nav resolution).
Modified files:
server/handlers/package_validate.go— surfaces validation + auto-synthesisserver/handlers/package_validate_test.go— 11 new testsserver/main.go— unified extension route registrationserver/pages/pages.go— matchSurface, evaluateAccess, findNavSurface, RegisterExtensionRoutes, nav status filterserver/pages/pages_surface_match_test.go— 11 new testsserver/pages/templates/base.html— surface path/params injectionsrc/js/sw/sdk/index.js— sw.navigate, popstate, version bumpdocs/PACKAGE-FORMAT.md— surfaces field documentationdocs/MULTI-SURFACE-GUIDE.md— developer guide
v0.8.5 — Extension Composability
Extensions can now compose with each other through declared slots, UI contributions, and cross-package function calls. This is the last kernel feature before 1.0 — the platform now supports the "extensions extending extensions" pattern.
Manifest declarations
- Surfaces declare named
slotsin their manifest (e.g.,toolbar-actions,note-footer) with context documentation for extension authors. - Extensions declare
contributesentries targeting{host}:{slot}names (e.g.,notes:toolbar-actions). Coupling is soft — install order doesn't matter. - The kernel validates slot/contribution conventions at install time.
Backend: lib.require() relaxation
lib.require()now works with any package that declaresexports, not justtype: "library"packages. A full package (with surfaces, settings, UI) can export callable functions for cross-package use.- The existing
dependsand permission model is unchanged — the called function runs with the target package's permissions.
Admin slots endpoint
GET /api/v1/admin/slotsreturns an aggregated map of all declared slots across installed packages with their contributors.- Uninstalling a package that declares slots warns about orphaned contributions in other packages.
SDK additions
sw.slots.renderAll(name, context)— convenience helper for host surfaces to render all components in a slot with error isolation.sw.slots.declare(name, description)— runtime slot declaration for discoverability and debugging.
Documentation
PACKAGE-FORMAT.md— addedslots,contributes,dependsfield docs.EXTENSION-GUIDE.md— new "Extension Composability" section with slot naming conventions, contribution patterns, and cross-package call examples.STARLARK-REFERENCE.md— updatedlibmodule to reflect exports-based calling (not library-type-only).
Modified files:
server/sandbox/lib_module.go— type check → exports checkserver/handlers/extensions.go— composability field validationserver/handlers/packages.go— orphaned contribution warningserver/main.go— admin slots route registrationsrc/js/sw/sdk/slots.js—renderAll(),declare(),declarations()docs/PACKAGE-FORMAT.md— slots, contributes, dependsdocs/EXTENSION-GUIDE.md— composability sectiondocs/STARLARK-REFERENCE.md— lib module updatedocs/DESIGN-extension-composability.md— status Draft → Implemented
New files:
server/handlers/admin_slots.go— admin slot aggregation endpoint
v0.8.4 — Documentation Refresh + Surface Sizing Fix
Eight versions of module additions (v0.7.5–v0.8.3) shipped without a docs pass. This release brings the public-facing guides up to date and fixes a CSS layout bug affecting all surfaces.
Documentation refresh
STARLARK-REFERENCE.md— addedworkspacemodule section (5 builtins),permissionsmodule section, andsettings.has_capability()documentation.EXTENSION-GUIDE.md— addedcapabilitiesmanifest block,vector(N)column type,user_permissionsandgate_permissionmanifest fields, updated sandbox permissions list with v0.8.0+ additions (files.read,files.write,workspace.manage).TUTORIAL-FIRST-EXTENSION.md— reviewed for v0.7+ accuracy (no changes needed).
Surface sizing fix
All surfaces using the shell topbar had scroll content clipped at the
bottom by ~44px (the topbar height). Root cause: surface containers were
siblings of #shell-topbar inside .surface-inner, which used
height: 100% without flex layout — the surface div claimed the full
parent height, ignoring the topbar sibling.
- Fix:
.surface-innernow usesdisplay: flex; flex-direction: columnso the topbar and surface share vertical space via flex layout. - All surface containers (
.surface-docs,.surface-admin,.surface-settings,.surface-editor,.extension-surface) changed fromheight: 100%toflex: 1; min-height: 0. - Inline styles on
surface-team-adminandwelcome-mounttemplates updated to match.
Modified files:
server/pages/templates/base.html—.surface-innergains flex column layoutsrc/css/surfaces.css—.surface-docs,.surface-admin,.surface-settings,.surface-editorsrc/css/extension-surface.css—.extension-surfaceserver/pages/templates/surfaces/team-admin.html— inline style fixserver/pages/templates/surfaces/welcome.html— inline style fixdocs/STARLARK-REFERENCE.md— workspace, permissions, has_capabilitydocs/EXTENSION-GUIDE.md— capabilities, vector, user_permissions, gate_permission
v0.8.3 — Vector Column Type
Extensions can now declare vector columns and perform similarity search. Three-tier progressive enhancement: native pgvector on Postgres, JSONB fallback without pgvector, TEXT fallback on SQLite.
Manifest: db_tables vector columns
- Declare
"vector(N)"as a column type (N = dimension, 1–4096). - On Postgres with pgvector: native
vector(N)type with auto-created HNSW index (vector_cosine_ops). - On Postgres without pgvector:
JSONBcolumn. - On SQLite:
TEXTcolumn (JSON-encoded float arrays).
Starlark API
db.query_similar(table, column, vector=[], limit=10, filters={}, metric="cosine")— returns rows ordered by ascending cosine distance with injected_distancekey.db.insert()now accepts list values (serialized as JSON strings) for vector column storage.
Internal
- Modified:
handlers/ext_db_schema.go—parseVectorDim,mapColTypegainshasPgvectorparameter, HNSW index creation for vector columns. - Modified:
sandbox/db_module.go—HasPgvectorinDBModuleConfig, list support instarlarkToGoValue,dbQuerySimilarwith pgvector and fallback paths,cosineDistancehelper. - Modified:
sandbox/runner.go— wireHasPgvectorfrom capabilities. - Modified:
handlers/extensions.go—SetCapabilitiesonExtensionHandler. - Modified:
server/main.go— wire capabilities to extension handler. - Updated:
docs/STARLARK-REFERENCE.md— vector similarity section. - New tests: 5 schema tests + 8 db module tests (13 total).
v0.8.2 — Capability Negotiation
Extensions declare environment requirements in their manifest. The kernel validates at install time and exposes a runtime query for graceful degradation.
Manifest: capabilities block
capabilities.required— array of capability names. Install is rejected (HTTP 422) if any are unavailable. Rollback deletes the package row.capabilities.optional— array of capability names. Install succeeds regardless; extensions query at runtime viasettings.has_capability().
Detected capabilities: postgres, pgvector, object_storage, s3,
workspace.
Starlark API
settings.has_capability(name)— returnsTrueorFalse. Always available (no permission required).
Admin API
GET /api/v1/admin/capabilities— re-probes and returns current state.
Internal
- New:
handlers/capabilities.go(detection, parsing, validation, admin handler). - New:
handlers/capabilities_test.go(14 tests). - New:
sandbox/settings_module_test.go(4 tests). - Modified:
handlers/packages.go— replaced stubcheckCapabilitieswith real validation;SetCapabilitiessetter. - Modified:
handlers/packages_bundled.go— bundled packages with unmet required capabilities are skipped on startup. - Modified:
sandbox/settings_module.go—has_capabilitybuiltin. - Modified:
sandbox/runner.go—capabilitiesfield +SetCapabilitiessetter. - Modified:
server/main.go—DetectCapabilitiesat startup, wired to runner and package handler, admin route registered.
v0.8.1 — Workspace Module
New workspace sandbox module. Managed disk directories for extensions
that need a real filesystem (git, compilers, media tools).
workspace module (permission: workspace.manage)
workspace.create(name)— create a workspace directory (idempotent). Returns absolute path. Enforces quota ifWORKSPACE_QUOTA_MB> 0.workspace.path(name)— get the absolute path of an existing workspace. Returns None if not found.workspace.list()— list workspace names for this extension.workspace.delete(name)— recursively remove a workspace (idempotent).workspace.usage(name)— disk usage in bytes (10-second timeout on directory walk).
Configuration
WORKSPACE_ROOT— mount point for extension workspaces (default/data/workspaces).WORKSPACE_QUOTA_MB— per-extension quota in MB (default0= unlimited).
Internal
- New file:
sandbox/workspace_module.go. - New permission constant:
ExtPermWorkspaceManage. - Config:
WorkspaceRoot,WorkspaceQuotaMBfields. - Runner wiring:
SetWorkspaceRoot()setter,buildModulesWithLibCtxcreates workspace module when permission granted. - Startup:
main.gocreates workspace root directory if writable, graceful degradation if not. - All directories scoped to
{WORKSPACE_ROOT}/{packageID}/{name}/. - Security: name regex (
^[a-z][a-z0-9_]{0,62}$),filepath.Clean+ prefix check,filepath.EvalSymlinksfor symlink escape detection. - 16 new tests (create, path, list, delete, usage, name validation, quota enforcement).
v0.8.0 — Files Module
New files sandbox module. Bridges the existing ObjectStore (PVC/S3) into
the Starlark sandbox with extension-scoped key namespacing.
files module (permissions: files.read, files.write)
files.put(name, content, content_type, metadata)— store a file with optional metadata companion. Accepts string or bytes content. 50 MB default limit (configurable viaEXT_FILES_MAX_SIZE).files.get(name)— read a file. Returns dict withcontent(bytes),content_type,size,metadata. Returns None if not found.files.meta(name)— read metadata only (no content transfer).files.list(prefix, limit)— list files by prefix. Returns list of dicts. Filters out internal_meta/companions.files.delete(name)— delete a file and its metadata companion. Idempotent.files.delete_prefix(prefix)— delete all files under a prefix.files.exists(name)— check existence without reading.
Internal
- New file:
sandbox/files_module.go. - New permission constants:
ExtPermFilesRead,ExtPermFilesWrite. ObjectStoreinterface gainsList(ctx, prefix, limit)method; implemented for PVC and S3.- Runner wiring:
SetObjectStore()setter,buildModulesWithLibCtxcreates files module when permission granted. - All keys scoped to
ext/{packageID}/. Metadata stored as companion JSON atext/{packageID}/_meta/{name}. - 16 new tests (15 files module + 1 PVC list).
v0.7.12 — Concurrent Execution Primitive
New batch sandbox module. Enables extensions to parallelize arbitrary
Starlark callables — including frozen library exports from lib.require().
batch module (permission: batch.exec)
batch.exec(callables, timeout=10)— runs up to 8 zero-arg callables concurrently, each in its ownstarlark.Threadwith independent step budget. Returns(results, errors)tuple with ordered results. Per-branch timeout (1–30s, default 10).- Nested
batch.exec()calls are prohibited (prevents exponential goroutine growth). lib.require()not available inside branches — load libraries before the batch call.print()output from branches is discarded.
Internal
- New file:
sandbox/batch_module.go. - New permission constant:
ExtPermBatchExec. - Runner wiring:
buildModulesWithLibCtxcreates batch module when permission granted. - Design doc:
docs/DESIGN-batch-exec.md. - 12 new tests (parallel ordering, partial failure, timeout, cancellation, cap enforcement, nesting prevention, frozen sharing, permission gating).
v0.7.11 — Query & HTTP Ergonomics
Four new Starlark sandbox builtins. No new permissions, no schema changes.
db module (permission: db.read)
db.count(table, filters={})— returns integer count of matching rows.db.aggregate(table, column, op, filters={})— single-value aggregation.op∈ {count, sum, avg, min, max}. Returns int, float, or None.db.query_batch(queries)— execute up to 10 query specs in a single call. Each spec supports the same parameters asdb.query.
http module (permission: api.http)
http.batch(requests)— concurrent HTTP dispatch of up to 10 requests. Individual failures return error response dicts (status: 0) rather than aborting the batch.
Internal
- Extracted
buildSelectQueryhelper fromdbQueryfor reuse bydb.query_batch. - 21 new tests (14 db, 7 http).
v0.7.10 — Workflow Handoff + Assignment UI
Closes the three UX gaps found during v0.7.9: public→team handoff, team inbox, and manual assignment. Also fixes dead system-admin bypass in team middleware and enriches assignment API responses.
Public Stage Handoff
RenderWorkflow()detects audience mismatch (team/system stage + unauthenticated visitor) and renders "Submitted Successfully" screen with reference ID instead of showing the team-gated form.
API Response Enrichment
ListByTeamandListMinehandlers enrich assignment records withworkflow_name,stage_name,sla_breachedby joining instance → workflow → version snapshot. Results cached per-request to avoid repeated DB hits.ListTeamInstancesreturnsinstanceViewwithworkflow_name,stage_name,age_seconds,sla_breached.
Team Middleware Fix
RequireTeamAdmin/RequireTeamMembersystem-admin bypass was dead code (c.Get("role")never set by auth middleware). Fixed to resolvePermSurfaceAdminAccessviaresolveAndCachePerms. VariadicallStoresparameter preserves backward compatibility.
Team Workflow Inbox
- Enhanced
AssignmentsTabin team-admin: "My Active" (claimed) and "Available" (unassigned) sections with claim/unclaim/release/work/complete actions, time-ago display, WS live updates. - Manual "Assign" button on unassigned rows with team member dropdown. New
POST /api/v1/assignments/:id/assignendpoint.
Assignment Notifications
- Engine calls
notifyAssignment()on assignment creation — notifies specific user or all team members via notification system.
SDK Gap Closure
- Added
workflowAssignmentsdomain (claim/unclaim/complete/cancel/assign/mine) - Added
teams.assignments,teams.workflowInstances,teams.cancelWorkflowInstance - Fixed dead
workflows.cancelroute referencing/channels/
E2E Test
ci/e2e-workflow-handoff.sh: public form → team review → claim → complete. Verifies audience mismatch screen, assignment creation, full lifecycle.
v0.7.9 — Workflow Independence Audit
Workflows proven independent of chat and all optional packages. Critical rendering bugs fixed, dead chat UI removed, deferred test debt closed.
RenderWorkflow Fix (critical)
RenderWorkflow()was a stub that never loaded instance/stage data — post-start page was broken. Now loads instance by token/ID, resolves current stage, populates all template fields.WorkflowPageDatafields renamed:ChannelID→EntryToken,ChannelTitle→WorkflowTitle,ChannelDescription→WorkflowDescription(vestigial chat-era names)- Stage modes aligned: template uses Go constants (
form,review,delegated,automated) instead of legacyform_only/form_chat - Dead chat UI removed: chat CSS, split layout,
sendMessage(), fallback chat branch (~180 lines deleted fromworkflow.html) - Landing page mode conditionals updated to match Go constants
- OpenAPI
stage_modeenum corrected (4 occurrences)
Bug Fixes (found during verification)
- Entry token resolution: handler passed route param (instance ID) as entry token to JS. Fixed: resolve actual token from
inst.EntryToken+?token=query param. - Fieldset submit guard:
submitForm()checkedFORM_TPL.fieldsbut not.fieldsets— progressive forms silently no-op'd. Fixed: accept either. - Stage advance status check: JS checked
result.status === 'advanced'but API returnsactive/completed. Fixed: on 200 OK withactive, reload to render next stage.
Deferred test coverage (carried from v0.7.6)
- 17 new SQLite store tests: workflow CRUD, stages, instances, lifecycle (advance/complete/cancel/stale), team scope, API tokens, users, groups
InstallPackagedecomposed from 400-line monolith into 7 private methods:receiveUpload,parseAndValidateArchive,extractPackageAssets,registerPackage,applySchemaAndPermissions,resolveDependencies,checkCapabilitiesci/e2e-workflow-nochat.sh— E2E test for workflow lifecycle without chat
Discovered issues (deferred to v0.7.10)
- Public→authenticated stage handoff: visitor sees auth-gated stage form instead of "submitted" screen
- Team member pickup UI: no surface for claiming workflow instances
- Assignment flow: no admin UI for manual instance assignment
Tests: 17 new store tests, 1 new E2E script
v0.7.8 — Bug Fixes & Admin Gaps
StartBySlughandler +/api/v1/workflow-entry/:scope/:slugroute for landing page Start button- Workflow delete guard: admin endpoint rejects team-scoped workflows (403)
- Package button cleanup: Delete hidden for bundled packages
- Package export:
fetch()with auth token instead ofwindow.open() - Settings CSS: bottom padding fix for save button cutoff
- Shared
StageFormcomponent between admin and team-admin; public entry URL with copy button
v0.7.7 — API Tokens + Extension Permissions
Personal access tokens (PATs) for programmatic API access, plus extension-declared user permissions for backend RBAC enforcement.
API Tokens (PATs)
- Migration 015:
api_tokenstable (PG + SQLite) with SHA-256 hash, prefix, JSON permissions, expiry - Token store interface + PG/SQLite implementations (Create, GetByHash, ListForUser, Revoke, CleanExpired, UpdateLastUsed)
POST /api/v1/auth/tokens— create token (returns plaintext once), permissions validated as subset of user'sGET /api/v1/auth/tokens— list my tokens;DELETE /api/v1/auth/tokens/:id— revokePOST /api/v1/admin/tokens— create token for any user (audit logged asadmin.token.create)- Auth middleware:
Bearer arm_pat_...tokens validated alongside JWTs, user active check, fire-and-forgetlast_used_atupdate - Permission scoping: PAT permissions used directly at request time (git model — retained until revoked)
auth.HashToken()shared SHA-256 utility (replaces localhashToken()in auth.go)- Settings UI: API Tokens tab with create form, permission checkboxes, copy-once display, revoke button
- Admin UI: "PAT" button on user rows creates tokens for any user
BootstrapPAT:ARMATURE_BOOTSTRAP_PAT=trueenv var creates admin PAT at startup, writes to/tmp/armature-admin-pat.txt- E2E smoke test: reads bootstrap PAT before falling back to login flow
Extension-Declared User Permissions
- Dynamic permission registry:
RegisterExtensionPermissions()/UnregisterExtensionPermissions()with RWMutex AllPermissionsWithExtensions()returns kernel + extension permissions;AllPermissionsGrouped()for admin UIuser_permissionsmanifest field: extensions declare user-facing permissionsgate_permissionmanifest field: ext_api.go checks user permission before callingon_requestreq["permissions"]in Starlark request dict: user's effective permissions included for inline checkspermissions.check(user_id, perm)Starlark module: read-only permission check, always available (no sandbox gate)- Group UI: permissions grouped by source (Platform / package name) with section headings
- Boot-time scan:
RegisterAllExtensionUserPermissions()populates registry from active packages - Uninstall cleanup:
UnregisterExtensionPermissions()called on package delete
Tests: 10 new tests (7 handler + 3 auth registry)
v0.7.6 — Code Hygiene + Test Coverage
Critical Fixes
- Removed
channelsfromallowedViewsindb_module.go—ext_view_channelsdoes not exist;db.view("channels")would crash - Fixed 5 dead API routes in
workflow.html— rewired to public workflow API (/api/v1/public/workflows/) - Fixed
RenderWorkflowhandler to pass route:idparam as entry token (was reading unsetchannel_id)
Dead Code Removal
- Deleted
SeedTestChannel()fromdatabase/testhelper.go(inserted into nonexistent channels table) - Removed
RunContext.ChannelIDfromsandbox/runner.go(vestigial, unused) - Removed
webhook.Payload.ChannelIDfield (channels no longer exist — breaking webhook JSON change) - Fixed stale comments in
storage.go,prometheus.go,workflow_module.goreferencing dead/channelspaths
Migration Hygiene
- Added SQLite placeholder
013_cluster_registry.sql(PG-only migration, aligns numbering) - Renumbered SQLite
013_test_runner_type.sql→014to match PG (compat rename inmigrate.go) - Documented missing migration 008 in both 009 files (merged into 007 during pre-1.0 consolidation)
Test Coverage
- 82 new workflow routing tests (
routing_test.go):ResolveNextStage,ResolveStageByName,ParseStageConfig,evaluateConditionwith all 10 operators - 10 new middleware tests (
permissions_test.go):RequirePermission,RequireAdmin, permission caching,RateLimiter(allow/deny/fail-open)
Bug Fixes
- Removed dead Admin "Storage" tab from System category (backend endpoint preserved for future Monitoring use)
- Fixed backup download:
sw.auth.token()→sw.auth._getToken()— both "Download Backup" and server backup download now work
v0.7.5 — Headless E2E + CI Gate
CI Gating Redesign
VERSIONandscripts/*no longer trigger frontend/backend tests — deploy-only (pipeline v0.18.0)docs/*changes now trigger build-and-deploy (docs are served in-app via Docs surface)- Path gating comment block updated to reflect corrected model
E2E Smoke Test
ci/e2e-smoke-test.sh— authenticates as admin, discovers surfaces, runs Playwright navigation testci/e2e-smoke-driver.js— visits every surface, asserts shell topbar present, no JS console errors, home link works- Screenshot-on-failure: full-page PNG + console log saved as CI artifacts
- Baseline screenshots captured for every surface (informational, not a gate)
CI Pipeline Integration
test-runnersstage re-enabled with broad trigger condition (BE/FE/packages/infra)- New
e2e-smokestage: boots server via docker-compose, runs Playwright smoke test, failure blocks merge docker-compose.ci.ymlgainse2e-smokeservice (same Playwright v1.52.0 image)build-and-deploynow depends on bothtest-runnersande2e-smoke
Documentation
docs/DESIGN-storage-primitives.md— v0.8.x storage primitives design (files module, workspace module, capability negotiation, vector columns)docs/DESIGN-extension-composability.md— v0.8.4 composability design (slots/contributes manifest fields, lib.require relaxation, SDK helpers)ROADMAP.mdexpanded through v1.0: v0.8.x storage primitives, v0.9.x reference extensions, v0.10.x sidecar tier, v1.0 gate criteria, design principles, full design decisions log
v0.7.4 — Documentation + Deferred Surface Work
Docs Category Grouping
- Backend
Categoryfield ondocEntrystruct inserver/handlers/docs.go - Frontend sidebar groups docs by category with
.docs-category-headingCSS - Four categories: Getting Started, Platform, Extension Development, Operations
- 14 docs in ordered list (was 7 ordered + 3 auto-discovered)
New Documentation (4 guides)
PERMISSIONS-AND-GROUPS.md— RBAC model, 7 permission slugs, system/custom groups, settings cascade, extension permissionsWORKFLOWS.md— Entry modes, stage modes/types/audiences, signoff gates, SLA enforcement, branch rules, Starlark hooksSTARLARK-REFERENCE.md— Sandbox constraints, 10 modules with function signatures and permission gates, example hook scriptFRONTEND-JS-GUIDE.md— Preact+htm runtime, 16 SDK modules with API reference, shell topbar patterns, CSS contract
Extension Guide Updates
config_sectionmanifest field documented: schema, backend discovery (configSectionsForSurface()), frontend__CONFIG_SECTIONS__contract, example component- Starlark Sandbox API section replaced with pointer to new Starlark Reference
Docs Content Refresh
- GETTING-STARTED:
sb_data→armature_datavolume name - ARCHITECTURE:
sb.register()/sb.ns()→swSDK references, shell topbar mention - DEPLOYMENT:
sb_storage→armature_storage, addedTLS_MODEenv var - TUTORIAL-FIRST-EXTENSION:
--bg-2→--bg-secondaryCSS variable - EXTENSION-CSS: self-hosted font notes on
--fontand--mono docs.go: addedAUDIT-andUSABILITY-prefix filters for auto-discovery
Team Admin Workflows Split
workflows.js(722 lines) split into 3 ES modules:workflows.js(~160 lines) —WorkflowsSection+WorkflowsTab+ importsworkflow-editor.js(~240 lines) —WorkflowEditor+StageFormworkflow-monitor.js(~210 lines) —AssignmentsTab+MonitorTab+SignoffPanel
- External import contract unchanged (default export stays in
workflows.js)
Bug Fixes
- Docs outline
scrollToHeadingnow scrolls.docs-contentcontainer instead ofscrollIntoView, preventing topbar from being pushed off-screen --bg-2(undefined CSS variable) replaced with--bg-secondaryinsw-shell.cssandsw-primitives.css
v0.7.3 — Extension Shell Migration
Shell Topbar Migration
- Migrated Chat, Notes, and Schedules from legacy
sw.shell.Topbarcomponent to the v0.7.0 shell topbar contract (sw.shell.topbar.setTitle/setSlot) - Eliminated double topbar (shell-injected + surface-owned) on all three extension surfaces
- Chat: reactive slot updates for thread title + People button when conversation changes
- Notes: slot content for + New Note, Import .md, and Graph toggle buttons
- Schedules: reactive slot with schedule count and + New Schedule button; removed legacy fallback branch
Runner Test Updates
- Added
shell-topbartest suite to chat-runner, notes-runner, and schedules-runner - Tests fetch surface JS and assert: no legacy
sw.shell.Topbarreference, usessw.shell.topbar.setTitle/setSlotAPI - 6 new tests across 3 runners
Package Versions
- Chat surface v0.3.0, Notes surface v0.9.0, Schedules surface v0.2.0
- Chat runner v0.2.0, Notes runner v0.2.0, Schedules runner v0.2.0
Roadmap
- Headless E2E automation moved to v0.7.5 (independent from shell migration)
v0.7.2 — Package Runners + CI Gate
Package Runners (5)
- Notes runner:
requires: ["notes"]. 3 suites (crud, folders, tags-search), 12 tests - Chat runner:
requires: ["chat", "chat-core"]. 2 suites (conversations, messaging), 9 tests - Schedules runner:
requires: ["schedules"]. 1 suite (crud), 5 tests - Workflow runner:
requires: ["content-approval"]. 1 suite (lifecycle), 5 tests - Renderer runner:
requires: ["mermaid-renderer"]. 1 suite (contract), 4 tests
Runner Result API
POST /api/v1/admin/test-runners/results— store structured run resultsGET /api/v1/admin/test-runners/results— retrieve latest results per runner- In-memory store with 4 Go handler tests
CI Integration
test-runnersstage in Gitea CI pipeline- Playwright driver launches headless browser, navigates to
/s/test-runners, triggers run-all wait-for-healthy.shpolls/healthz/readybefore test execution- DinD networking fix: resolve container IP via
docker inspect(port mapping not exposed to runner localhost)
v0.7.1 — Surface Runner Framework
sw.testing SDK Module
- New kernel SDK module at
src/js/sw/sdk/testing.js sw.testing.suite(name, fn)— register test suites with lifecycle hookssw.testing.run(name?)— execute one or all suites, returns structured JSON- Suite context:
s.test(),s.beforeAll/afterAll(),s.beforeEach/afterEach(),s.track(),s.skip() - Test context:
t.assert.ok/eq/neq/gt/match/throws/status/shape/arrayOf,t.warn(),t.skip() - Auto-cleanup:
track(type, id)registers resources for LIFO deletion in afterAll - Three result statuses: passed / failed / warned — warnings are never silent
test-runner Manifest Type
- New
"type": "test-runner"inValidateManifest— surface-like packages discovered by type - Test-runner packages excluded from sidebar nav (not type "surface" or "full")
- Runner manifests support
"requires": [...]— missing packages → clean skip
ICD Runner Migration
- Migrated from hand-rolled
T.test()/T.assert()framework tosw.testing.suite() - Stripped extension-dependent tests (channels, notes, personas, etc.) — those belong in v0.7.2 package runners
- Kernel-only suites: smoke, crud (admin, profile, notifications, teams, workflows, extensions, surfaces, packages), authz, security, providers, packaging, sdk
- Type changed to
"test-runner", standalone route removed, UI rendering delegated to registry
SDK Runner Migration
- Migrated from
T.dualTest()/T.domainsframework tosw.testing.suite() - Dual-path validation preserved: SDK call + raw ICD fetch + verdict dispatch
- Stripped extension domains — kernel-only suites: misc, workflows, admin, packages, connections, dependencies, composition
- SHAPE_BUG verdict maps to
t.warn(), SDK_BUG/ICD_BUG tot.assert.ok(false)
Runner Registry Surface
- New
test-runnerssurface at/s/test-runners(admin-only) - Discovers installed test-runner packages via admin packages API
- Dynamically loads each runner's JS to register suites
- Run All button, per-runner Run button, real-time results dashboard
- Suite/test results with pass/fail/warn/skip color coding, timing, error details
- Export Failures / Export Full Results buttons for JSON download
requireschecking with prominent skip display for missing dependencies- Dark mode styling using kernel CSS variables
Database Migration
- SQLite migration 013: adds
test-runnerto packages.type CHECK constraint - Postgres migration 014: same constraint update
v0.7.0 — Shell Contract + Surface Audit + Rebrand
Shell Infrastructure
- Kernel-injected two-slot topbar for all surfaces (home, left slot, center slot, bell, user menu)
sw.shell.topbarSDK API:setLeft(),setSlot(),setTitle(),hide(),show().sw-topbar__tabs/.sw-topbar__tabCSS classes for consistent tab styling- Shell topbar auto-mounts on extension surfaces via
#shell-topbardiv
Backend WS Events
package.changedevent broadcast on install/uninstall/enable/disable/updateauth.changedevent targeted to affected user on team/group membership changesnotification.all_readevent split fromnotification.readfor cleaner badge syncHub.Broadcast()method for untargeted all-client events
User Menu + Bell Reactivity
- User menu re-fetches surface list on
package.changedandauth.changedevents - Notification bell syncs on
notification.readandnotification.all_readacross tabs
Surface Migrations
- Settings: Pattern B (flat tabs in topbar, no sidebar, full-width content)
- Admin: Pattern C (category tabs in topbar, surface-owned sidebar below)
- Team Admin: Pattern B (flat tabs, no sidebar, Groups tab removed)
- Docs: Pattern A (shell topbar auto-renders, removed explicit Topbar import)
- All 4 surfaces now have notification bell and user menu via shell topbar
Error Handling + Empty States
.sw-inline-errorCSS primitive for inline error + retry pattern.sw-empty-stateCSS primitive for guided empty states- Admin Workflows, Packages, Groups: inline error on list fetch failure
- Admin Workflows, Groups: descriptive empty state guidance
Announcement Global Dismiss
- Announcement dismiss state persisted to localStorage keyed by content hash
- Dismissed once on any surface, dismissed everywhere
Rebrand Assets
favicon-light.svgrenamed towordmark.svg(was a 520x80 wordmark, not an icon)- New
favicon-light.svg: actual square light-mode icon - New
wordmark-dark.svg,wordmark-light.svgfor dark/light backgrounds - New
favicon-light-32.png,favicon-light-256.pngraster icons - Full icon library deployed to
src/icons/(both b/e variants, animated SVGs) manifest.jsondescription updated to "Self-hosted extension platform"- Light-mode icon entries added to PWA manifest
Bug Fixes
- Docs: "On this page" outline links now scroll to headings (IDs were missing from rendered HTML)
- ICD security tier: tightened path traversal assertion (400/422, not 409), added
finallycleanup - Workflow demo: replaced silent catch with inline error + retry
- Team Admin: signoff panel shows display names instead of raw UUIDs
- Deleted
packages/hello-dashboard/(dead package) - Deleted
team-admin/groups.js(37-line dead-end, no CRUD)
v0.6.18 — CI Bundle Wiring
Wire BUNDLED_PACKAGES env var into the Gitea CI pipeline so each
environment gets the correct package set at boot.
Changed
- CI: dev deploys —
BUNDLED_PACKAGES=*(install all, matches docker-compose default). - CI: test deploys —
BUNDLED_PACKAGES=notes,chat,chat-core(core surfaces only). - CI: prod deploys —
BUNDLED_PACKAGES=notes,chat,chat-core,mermaid-renderer,schedules. - docker-compose.yml — default
BUNDLED_PACKAGESchanged from empty to*(install all for local dev).
Fixed
- TestBundledInstall_DefaultAllowlist — updated test assertions to match v0.6.17's empty default set (was still expecting
notesin curated defaults).
v0.6.17 — Bug Fixes & Welcome Logic
Fixes broken UI interactions (folder creation, team member add), dropdown overflow, welcome surface auto-disable, and bare-install default behavior.
Fixed
- Notes "Add folder" button —
prompt()replaced withsw.prompt()so the dialog renders correctly in the extension iframe sandbox. - Admin "Add team members" — user list API returns
{data:[…]}; handler now unwraps the envelope (Array.isArray(u) ? u : u.data) so the user picker populates. - Package filter dropdown overflow — removed
right:0constraint on.sw-dropdown__list, addedmin-width:max-contentandoverflow-x:hiddenso option labels ("Extension", "Workflow") render fully without a scrollbar. - Admin actions cell wrapping — switched
.admin-actions-cellfromwhite-space:nowrapto flexbox withflex-wrap:wrap; gap:4pxso buttons don't overflow on narrow viewports.
Changed
- Welcome surface auto-disable — welcome page now redirects to
/when any non-core extension surface is installed. Removed from the topbar navigation surface list so it never appears alongside real surfaces. - Zero default bundled packages —
defaultBundledPackagesmap is now empty. Fresh installs start bare; useBUNDLED_PACKAGESenv var to control what gets auto-installed per environment (*for all, comma-separated list for selective). - Bundled filter logic —
nil(from*) means install all; empty map (default) means install nothing. Previous code treated both as "install all". - User menu conditional items — Docs, Settings, and Team Admin menu entries only appear when those surfaces are actually enabled, not assumed.
- SDK imperative host mount —
ToastContainerandDialogStackare now auto-mounted by the SDK boot sequence for extension surfaces that lack an AppShell, preventing missing toast/dialog hosts.
v0.6.16 — Usability Survey Gate
Machine-auditable UI quality gate. Four new audit scripts, a structured survey prompt, contrast and touch-target fixes, and Docker Hub documentation correction.
Added
scripts/generate-ui-inventory.sh— walks all kernel + package CSS, extracts every class selector with surface, line number, responsive breakpoints, spacing tokens, and font-size usage. Outputsui-inventory.json(1567 entries).scripts/check-contrast.sh— parsesvariables.cssdark/light token pairs, computes WCAG AA contrast ratios for 24 semantic text-on-background pairings per theme (48 total). Uses AA (4.5:1) for normal text, AA-lg (3.0:1) for large/bold text contexts.scripts/generate-coverage-matrix.sh— 12 kernel primitives × all surfaces markdown table. Flags any deprecated component usage (.btn-primary, etc.).scripts/audit-touch-targets.sh— static analysis for 44px minimum mobile touch targets on close buttons and interactive elements.docs/USABILITY-SURVEY.md— structured 8-section prompt (viewport, banners, responsive, styling, contrast, touch targets, focus indicators, component uniformity) with pass/fail criteria and file paths for automated execution.- Focus indicators —
:focus-visiblestyles on.sw-btn,.sw-input,.sw-dropdown__trigger,.sw-menu__item,.sw-tabs__tab. - Mobile touch targets —
min-width/min-height: 44pxin@media (max-width: 768px)for.sw-banner__close,.sw-toast__close,.sw-dialog__close,.sw-drawer__close,.sw-tabs__arrow,.modal-close,.sw-tabs__tab,.sw-dropdown__option.
Fixed
- WCAG contrast violations — dark-mode accent darkened from
#6c9fffto#6493ed(3.03:1 with white text), dark-mode success from#22c55eto#1dab51(3.00:1). Light-mode--text-3darkened from#8b8da3to#787a92. Light-mode--success-lightand--warning-lightdarkened for badge contrast. All 48 pairings now pass. - Docker Hub references —
docs/DEPLOYMENT.mdanddocs/DISTRIBUTION.mdcorrected fromghcr.io/armature/armaturetogobha/armature(Docker Hub). Builder image corrected togobha/armature-builder. GitHub URL corrected togithub.com/gobha/armature.
v0.6.15 — User Display Audit
Every user-facing identity surface now shows human-readable names instead of
UUIDs, with the canonical fallback chain: display_name → username → "Unknown".
Added
GET /api/v1/users/resolve?ids=...— batch endpoint returns identity records (username, display_name, handle, avatar_url) for up to 100 user IDs. Response keyed by ID for O(1) client lookups.sw.usersSDK module —resolve(id),resolveMany(ids),displayName(user)with 60-second local cache. Surfaces use this instead of ad-hoc lookups or stale snapshots.- 5 handler tests for the resolve endpoint (single, multiple, missing, empty, no-param).
Changed
- Admin users list — shows
display_name || usernameas primary identifier, with username shown as secondary when display_name is set. - Admin teams/groups member lists — replaced
username || user_idwithdisplay_name || username || 'Unknown'. - Team-admin members — dropdown and list now show display_name.
- Chat participants — resolved from users table via
sw.users.resolveMany()instead of relying on creation-time snapshot. Message sender names, typing indicators, and participant sidebar all use resolved names. - Dashboard greeting — added
|| 'Unknown'terminal fallback. - Team activity log — capitalized fallback to
'Unknown'.
Deprecated
participants.display_namecolumn in chat-core — column retained for backward compatibility but UI no longer relies on snapshot values. Comments added topackages/chat-core/script.starnoting deprecation.
v0.6.14 — Visual Polish
Systematic cleanup of stale values, self-hosted fonts, and rendering fixes. Final visual pass before the v0.6.15 usability survey gate.
Fixed
- v0.6.13 spacing regressions — added half-step tokens (
--sp-1h6px,--sp-2h10px) and restored correct padding on.sw-btn--sm,.sw-input,.sw-menu__item,.sw-dropdown__option,.sw-tabs__tab. - Theme settings toggle — showed resolved theme ("Dark") instead of stored
mode ("System"). Changed
appearance.jsto readsw.theme.mode. - Notes surface scrollbar — added
overflow: hiddento.surface-innerinbase.html, preventing spurious scrollbar at any scale. - Chat input clipped at high scale — same
overflow: hiddenfix prevents zoomed content from overflowing the surface container. - User menu drift at scale > 100% —
menu.jsnow dividesgetBoundingClientRect()coords by the CSS zoom factor, fixing position forposition: fixedmenus inside a zoomed ancestor. - Undefined variables —
--text-secondary(login),--text-muted(user picker),--text-1(settings toggle) replaced with correct tokens.
Changed
- Stale fallback colors purged — removed ~65 hex/rgba fallback values
from
var()calls across 9 kernel CSS files and 3 extension packages. Old gold theme color#b38a4efully eliminated (9 instances). - Self-hosted fonts — bundled DM Sans and JetBrains Mono woff2 files
in
src/fonts/. Replaced Google Fonts@importand login.html<link>with local@font-facedeclarations. Zero external font dependencies. - Consistent border-radius — added
--radius-sm: 4pxtoken. Migrated ~60 hardcodedborder-radiusvalues across all kernel CSS and 12 extension packages to three tokens:--radius-sm(4px),--radius(8px),--radius-lg(12px).
Updated
docs/EXTENSION-CSS.md— added--sp-1h,--sp-2hhalf-step tokens and--radius-smto the public CSS contract.
v0.6.13 — Responsive & Spacing
Spacing token scale and tablet breakpoint. All kernel CSS and extension packages migrated from hardcoded values to design tokens.
Added
- Spacing tokens (
--sp-1through--sp-12) — 4px-grid scale invariables.css. Nine stops: 4, 8, 12, 16, 20, 24, 32, 40, 48px. Numeric naming (--sp-N), rem-based for zoom/font-size respect. - Tablet breakpoint (
max-width: 1024px) — new responsive tier between mobile (768px) and desktop. Secondary workspace pane narrows to 360px, admin sidebar to 120px, settings/admin/editor navs shrink. - Breakpoint documentation in
EXTENSION-CSS.md— Mobile (768px), Tablet (1024px), Desktop (default). - Spacing guidelines in
EXTENSION-CSS.md— token table with computed pixel values and usage examples.
Changed
- 8 kernel CSS files migrated to spacing tokens —
sw-primitives.css,modals.css,surfaces.css,layout.css,sw-shell.css,primitives.css,user-menu.css,sw-login.css. Hardcoded padding, margin, and gap values replaced withvar(--sp-N). - 12 extension packages migrated — chat, dashboard, editor, git-board, hello-dashboard, icd-test-runner, notes, schedules, sdk-test-runner, tasks, team-activity-log, workflow-demo.
- Login hero breakpoint normalized from 900px to 1024px (tablet).
- Notes mobile breakpoint normalized from 700px to 768px (standard).
v0.6.12 — Extension CSS Isolation
Prefix enforcement prevents extension CSS from leaking into the kernel or
sibling extensions. All 12 in-tree packages migrated to .ext-{slug}-*
naming convention.
Added
data-extattribute on extension mount container — enables scoped selectors like[data-ext="chat"] .ext-chat-app.- CSS linter (
scripts/lint-package-css.sh) — validates that the first class selector in every extension CSS rule starts with.ext-{slug}. Exempts:root,@keyframes,@font-face,@media, kernel.sw-*classes, and CodeMirror.cm-*classes. - Kernel CSS contract (
docs/EXTENSION-CSS.md) — documents stable public classes and CSS variables that extensions may reference. Everything else is internal kernel CSS.
Changed
- 12 packages migrated — all class selectors renamed to
.ext-{slug}-*: chat, dashboard, editor, git-board, hello-dashboard, icd-test-runner, notes, schedules, sdk-test-runner, tasks, team-activity-log, workflow-demo. CSS and JS files updated in lockstep. icd-test-runner— ID selectors (#extension-mount) converted to class-based selectors with proper prefix.editorcross-references — compound selectors referencing notes classes updated to new.ext-notes-*names.chatkernel overrides —.sw-dialog:has(...)override scoped under[data-ext="chat"]instead of global.
v0.6.11 — CSS Deduplication
One class per concept. The old primitives.css button, toast, popup-menu,
dropdown, and tabs systems are retired. sw-primitives.css is the single
source of truth for all Preact component styles.
Changed
- Buttons: All 29 files migrated from
.btn-primary/.btn-small/.btn-danger/.btn-ghost/.btn-md/.btn-smto the BEM-style.sw-btn .sw-btn--{variant} .sw-btn--{size}system. - Toasts: Old
.toast-container/.toastCSS deleted. SDK'ssw.toast()API already used.sw-toast-*classes — no JS changes. - Popup menus: Old
.popup-menu/.popup-menu-itemCSS deleted (unused —.sw-menuis the active system). - Dropdown collision resolved: Old
.sw-dropdown(styled<select>) deleted fromprimitives.css. Thesw-primitives.csscustom dropdown component (.sw-dropdownwith BEM sub-elements) is authoritative. - Tabs collision resolved: Old
.sw-tabs/.sw-tab-btndeleted fromprimitives.css. Thesw-primitives.cssscrollable tabs component (.sw-tabs__tab) is authoritative. .settings-sectioncollision resolved: Removed duplicate definition frommodals.css. Thesurfaces.csscard-style definition is authoritative;.settings-content .settings-sectionoverride resets card styling for flat settings layouts.
Added
.sw-btn--successvariant insw-primitives.css(green action button).--bg-activeCSS variable in both dark/light themes (variables.css).scripts/audit-css-collisions.sh— finds duplicate class selectors across kernel CSS files and outputs a JSON collision report.
Fixed
packages/sdk-test-runner/css/main.css: Wrong variable names (--text3→--text-3,--text2→--text-2,--bg1/--bg2→--bg-raised).packages/icd-test-runner/css/main.css: Replaced inline button fallback styles with kernel.sw-btnsystem.
Removed
- Old button classes:
.btn-primary,.btn-small,.btn-danger,.btn-full,.btn-ghost,.btn-subtle,.btn-sm,.btn-md. - Old toast classes:
.toast-container,.toast,.toast.error/warning/success. - Old popup menu classes:
.popup-menu,.popup-menu-item,.popup-menu-*. - Old dropdown and tabs definitions from
primitives.cssthat collided withsw-primitives.css.
v0.6.10 — Viewport Foundation
Single layout model. Every surface renders inside one containment chain:
body → shell → surface. No dual systems. No transform hacks.
Changed
- CSS
zoomreplacestransform: scale(): UI scale (80%–175%) now uses CSSzoomon#surfaceInnerinstead oftransform: scale().zoomreflows layout correctly —getBoundingClientRect()returns accurate values, eliminating the scale-correction hack inmenu.js. Supported in all evergreen browsers (Firefox 126+, June 2024). - Single layout root:
<body>inbase.htmlis the authoritative flex column layout..sw-shellCSS demoted from viewport-level container (height: 100vh) to fill-parent (height: 100%). Safe-area insets moved from.sw-shellto<body>. - Banner single source of truth: Template banners measure their own
height via inline
<script>and set--banner-top-height/--banner-bottom-heightCSS variables. Removed--banner-h: 28pxfixed variable.ShellBannerPreact component'suseEffectmeasurement removed (dead code — no surface importsAppShell). sw-shell__bannerposition: Changed fromposition: fixedtoposition: static— template banners are in-flow elements.sw-shell__bodypadding: Removedpadding-top/bottomfor banner offsets — unnecessary with in-flow banners.- Extension surfaces
100vh→100%:chat-app,chat-loading,surface-dashboardnow useheight: 100%to inherit from the extension mount container (like Notes). Fixes overflow behind banners. 100vh→100dvhfallbacks: All viewport-height declarations useheight: 100vh; height: 100dvh;pattern for correct behavior on mobile browsers. Affects:base.html,sw-login.css,workflow.html,workflow-landing.html,primitives.css,git-board/css/main.css.sw.shell.getScale()deprecated: Returns1always — CSSzoomhandles layout reflow without manual correction.
Deprecated
src/js/sw/shell/app-shell.js,app.js,surface-viewport.js— no surface imports these. Layout root is<body>inbase.html.
v0.6.9 — Session Lifetime Config
Admin-configurable session durations, "keep me logged in" opt-in, and optional idle timeout. Completes the auth hardening started in v0.6.8.
Added
- Admin session settings:
session.access_token_ttl(default 15m, clamp 5m–60m) andsession.refresh_token_ttl(default 7d, clamp 1h–90d) stored inglobal_settings. NewLoadSessionConfig()helper reads and clamps values withparseDurationString()supportingm,h,dsuffixes. - "Keep me logged in" checkbox: Login form opt-in. Checked = full
refresh_token_ttl. Unchecked = capped at 24h. Cookiemax-agetracks whichever lifetime was chosen.keep_loginflag stored on refresh token row. - Config-driven token generation:
generateTokens()reads TTLs fromglobal_settingsinstead of hardcoded15*time.Minute/7*24*time.Hour. Response includesexpires_inandrefresh_expires_in(seconds) so the client schedules refresh correctly. - Idle timeout (optional): Admin-toggleable (default off). Server
checks
last_activity_aton refresh-token row; rejects if gap exceedssession.idle_timeout. Client SDK pingsPOST /api/v1/auth/activityon click/keydown (debounced, max 1/min). - Admin Settings > Session section: Dropdowns for access TTL, refresh TTL, and idle timeout with toggle.
- 7 new tests: Duration parsing, clamping (low/high), defaults, invalid values, empty idle timeout.
Changed
CreateRefreshTokenstore method now acceptskeepLogin boolparameter; both Postgres and SQLite implementations updated.GetRefreshTokenInforeturnsRefreshTokenInfostruct withUserID,KeepLogin,LastActivityAtfor idle-timeout decisions.- SDK
auth.login()accepts optional thirdkeepLoginparameter; cookie max-age derived from serverrefresh_expires_inresponse. - SDK boots activity tracking after successful auth boot.
v0.6.8 — Cookie Fix + UI Hardening Roadmap
Fixed
- Session cookie max-age bug:
arm_tokencookie was set to 15 min (matching access token) while refresh token lasted 7 days. Cookie now matches refresh token lifetime so Go SSR middleware can serve page shells while JS refreshes the access token client-side.
Added
- ROADMAP-UI.md: Detailed UI hardening roadmap (v0.6.9–v0.6.15) covering session config, viewport foundation, CSS deduplication, extension CSS isolation, responsive layout, visual polish, and automated usability survey gate.
v0.6.7 — Native mTLS
End-to-end mutual TLS without a reverse proxy. Targets systemd+podman deployments where the Go binary terminates TLS itself.
Added
TLS_MODEconfig: Three values —none(default, plain HTTP),server(TLS, no client cert),mtls(mutual TLS, client cert required). Independent ofAUTH_MODE. Combinations:server+builtinfor HTTPS with password auth,mtls+mtlsfor full mTLS identity,none+mtlsfor proxy-terminated (existing behavior).- TLS server mode: Binary calls
ListenAndServeTLSdirectly whenTLS_MODEisserverormtls. TLS 1.3 minimum, no cipher suite configuration. Config:TLS_CERT,TLS_KEY,TLS_CApath env vars. MTLSNativeProvider: New auth provider that readsr.TLS.PeerCertificates[0]directly — no header trust.Subject.CommonNamebecomes username,sha256(cert.Raw)becomesexternal_id. Auto-provisions users identically to the proxy provider. Selected whenAUTH_MODE=mtlsandTLS_MODE=mtls.- Shared mTLS helpers:
ParseDN(),FingerprintCert(), andresolveOrProvision()extracted tomtls_helpers.go. Both proxy and native providers consume the same user resolution logic. - Peer TLS config:
BuildPeerTLSConfig()constructs a*tls.Configfor outbound node-to-node connections (forward-looking — cluster registry is currently DB-backed with no HTTP peer calls). armature-ca.sh: Shell wrapper around openssl for cert provisioning. Three commands:init(CA keypair),issue-node(365d, ServerAuth + ClientAuth EKU),issue-user(90d, ClientAuth only). All ECDSA P-256, PEM output.- 12 new tests:
FingerprintCertdeterminism, native provider unit tests (nil TLS, empty certs, no CN, email extraction), TLS integration tests (no cert rejected, wrong CA rejected, expired cert rejected, valid cert accepted, peer certificate visibility).
Changed
MTLSProvider→MTLSProxyProvider: Renamed for clarity. Config typeMTLSConfig→MTLSProxyConfig. ConstructorNewMTLSProvider→NewMTLSProxyProvider. Filemtls.go→mtls_proxy.go.
v0.6.6 — Final Hardening
Final pass before public release. Security, correctness, and developer experience. No new features — only fixes, validation, and cleanup.
Added
ValidateManifest()gate: Centralized manifest validation function (package_validate.go) called at both upload-install and bundled-install time. Catches malformed manifests early with clear error messages. 12 unit tests covering all type constraints and edge cases.- Extension dependency auto-activation: Installing a package whose
dependencieslist an uninstalled library will auto-install that library from the bundled packages directory. If the dependency is not bundled, the error message lists exactly what's missing. OptionalAuthmiddleware: New page middleware for workflow visitor routes. Authenticates if a token is present, allows anonymous pass-through otherwise. Replaces the staleAuthOrRedirectTODO for Session routes.- Package signing schema reservation:
signaturefield accepted in manifest (no-op).PACKAGE_VERIFY_SIGNATURESenv var logs warnings for unsigned packages when enabled. No cryptographic verification yet — schema slot reserved to prevent a breaking change later. - ICD/SDK runner v0.6.x coverage: Smoke tier adds metrics, cluster, backups, docs, and OpenAPI JSON endpoints. SDK admin domain adds metrics, cluster, and backups dual-path tests.
Fixed
- OIDC nonce validation: ID token nonce claim is now validated against the stored authorization request nonce. Previously the nonce was generated and stored but never checked on callback — a token replay/substitution vulnerability.
Changed
- Schema migration stub:
RunSchemaMigrations()no longer pretends to work. Replaced with a log-only function documenting that only additive schema changes are supported. Downgrade rejection preserved. - Session middleware TODO resolved:
main.goSession slot now usesOptionalAuthinstead ofAuthOrRedirect. OIDC nonce TODO and migration stub TODO also resolved.
v0.6.5 — Renderer Pipeline + Docs Rewrite
Lifts block rendering to a kernel SDK primitive so all surfaces share one markdown pipeline. Rewrites docs for external audience. Adds CONTRIBUTING guide and extension tutorial.
Added
sw.renderersSDK module: Kernel-level renderer registry. Extensions register block renderers (fenced code blocks) and post renderers (DOM post-processing) once; all surfaces consume viasw.markdown.sw.markdownSDK module: Unified markdown rendering viamarkedv16 (vendored). Lazy-loadsmarked+ DOMPurify. Custom code hook delegates fenced blocks tosw.renderers. Wikilink tokenizer ([[Page Name]]) built in for all surfaces.- Browser extension script loader: Server injects
<script>tags for enabled browser-tier extensions into all pages. Extensions register viasw:readyevent. - Chat markdown support: Messages with
content_type: 'markdown'render viasw.markdown. Existingtextmessages stay plain (no behavior change). - Mermaid architecture diagrams: Six diagrams in
docs/ARCHITECTURE.md: system overview, request flow, extension lifecycle, realtime events, settings cascade, cluster topology. CONTRIBUTING.md: Development setup, project structure, test commands, code conventions.docs/TUTORIAL-FIRST-EXTENSION.md: Step-by-step guide to building and installing a browser extension.docs/DESIGN-WORKFLOWS.md: Clean workflow system design document.- 13 new renderers tests (
src/js/__tests__/renderers.test.js).
Changed
- Notes surface: Hand-rolled markdown renderer replaced with
sw.markdown.renderSync(). Post-renderers (mermaid, katex, etc.) now execute on preview. ~90 lines deleted. - Docs surface: Hand-rolled markdown renderer replaced with
sw.markdown.renderSync(). ~160 lines deleted. - 4 renderer extensions (mermaid, katex, csv-table, diff-viewer):
Rewritten from dead
Extensions.register()pattern to IIFE +sw.renderers.register()viasw:readyevent. All rendering logic preserved. - Surface alias routes removed: Six
/api/v1/admin/surfaces/*backward compat routes deleted frommain.go. SDK and ICD runner migrated to canonical/api/v1/admin/packages/*endpoints. - Docs rewritten for external audience: All references to fork history, internal codenames, and pre-fork version numbers removed from docs, CHANGELOG, and ROADMAP.
Removed
docs/DESIGN-WORKFLOW-REDESIGN-0.2.6.md— replaced byDESIGN-WORKFLOWS.md.- Hand-rolled markdown renderers in Notes and Docs surfaces.
- Six
/admin/surfaces/*API alias routes.
v0.6.4 — Admin Health/Metrics Tab + Cluster Merge
Structural consolidation: cluster dashboard merges into Admin as a Health tab. Comprehensive metrics endpoint for runtime, DB, cluster, and extension stats.
Added
- Admin Health tab: New "Health" section under Monitoring in the Admin surface. Auto-refreshing panels for runtime, database, cluster, and extension metrics with configurable poll interval (5/10/30/60s).
GET /api/v1/admin/metrics: Single JSON endpoint returning all platform metrics — runtime (goroutines, heap, GC, uptime, FDs), DB pool (latency, active/idle/max, PG-only dead tuples + active backends), cluster nodes (when PG multi-node), and extension stats (Starlark exec/errors/duration, trigger fires, event bus publish/deliver counts).- Fattened heartbeat payload: Cluster heartbeat JSONB now carries stack usage, GC CPU%, extensions loaded, Starlark execution counters, and trigger fire count — visible in per-node cluster cards.
- Sandbox execution tracking:
ExecPackageandCallEntryPointnow increment atomic counters (exec, errors, duration) and the previously declared but unusedSandboxExecutionsTotalPrometheus counter. - Event bus counters:
Bus.PublishCount()andBus.DeliverCount()track cumulative publish/deliver operations. - Trigger fire counter:
Engine.FireCount()tracks cumulative trigger fires across webhook, event, and scheduled triggers. - 4 new handler tests (metrics SQLite shape, cluster shape, extension counters).
- 1 new cluster test (fattened heartbeat payload).
Changed
- Health endpoint consolidation:
/healthand/api/v1/healthnow use a sharedbuildHealthResponse()function returning identical JSON. Both now includeregistration_enabledwhen the database is connected. - Block renderer
requiresremoved:mermaid-renderer,katex-renderer,csv-table, anddiff-viewerno longer require["chat"]— they activate on any surface (notes, docs, etc.).
Removed
cluster-dashboardpackage: Standalone surface package retired. Cluster node visibility is now in Admin > Monitoring > Health.
v0.6.3 — Dead Code Sweep + Registry Fix
Hardening: fix broken registry install, add registry settings UI, clean up dead code, narrow default bundle.
Fixed
- Registry install: SDK was sending
{ url }but the Go handler expects{ download_url }, causing every registry Install click to return 400.
Added
- Package Registry settings: New "Package Registry" section in Admin > Settings with a URL input field for configuring the external registry.
scripts/generate-registry.sh: Shell script that reads.pkgZIPs, extracts manifests, and emits aregistry.jsonfor self-hosted discovery.docs/PACKAGE-REGISTRY.md: Documents registry JSON format, admin configuration, script usage, and self-hosting setup.
Removed
- Dead Go code: Orphaned channel/message type comments from
interfaces.go, unusedroleFilterType()frompages.go, stale version comment frommain.go. - Dead vendor JS:
marked.min.js(40KB) andpurify.min.js(22KB) — unused vendor copies with zero production imports. Removed cache entries fromsw.js. dev.html: 676-line standalone dev gallery, not referenced by anything.- Stale version comments: Stripped legacy version annotations across ~140 files — standalone comments, inline suffixes, section headers, and config field docs. Reworded to describe features rather than reference obsolete internal version numbers.
Changed
- Default bundle narrowed from 10 packages to 5:
notes,chat,chat-core,mermaid-renderer,schedules. All other packages remain available viaBUNDLED_PACKAGES=*or explicit list.
v0.6.2 — Docs Polish + Dynamic OpenAPI
Docs surface polish and dynamic OpenAPI spec with extension route merging.
Added
- Dynamic OpenAPI spec:
GET /api/docs/openapi.jsonbuilds a merged spec at request time — kernel endpoints + auto-generated stubs for every extensionapi_routesentry. Extensions may optionally declareapi_schemainmanifest.jsonfor richer path items (params, body, response examples). api_schemamanifest field: Optional array inmanifest.json. Malformed entries are logged and skipped — never blocks extension loading.- Tests: 7 new handler tests (zero extensions, stubs, rich schema, multi-extension, malformed schema, disabled exclusion, required fields).
Fixed
- Dark mode contrast: Replaced all hardcoded light-mode fallbacks in docs CSS with theme-aware CSS variables. Tables, code blocks, nav items, and headings now readable in dark mode.
- Docs loading state: Replaced borrowed
settings-placeholderanimation with docs-specific pulse skeleton. Added error state with retry button. - Docs routing: Default route (
/docs) now redirects to/docs/GETTING-STARTEDinstead of the blank/:sectionplaceholder. - Docs scrolling: Content area now scrolls correctly — full document visible without text overflowing viewport.
- Docs icon: Changed from 🧩 to 📖 in the user menu.
- Docs duplicate menu entry: Docs was appearing twice (once from surfaces
API, once from hardcoded items). Added
'docs'toCORE_IDSfilter. - Swagger UI: Updated to point at dynamic
/api/docs/openapi.jsonendpoint. Static YAML preserved for backward compatibility. - OpenAPI tests on Postgres:
openapiTestStores()was hardcoded tosqlite.NewStores()— causedpq: syntax error at or near ","in CI. Now usesdatabase.IsSQLite()check like all other handler tests.
API
| Endpoint | Method | Description |
|---|---|---|
/api/docs/openapi.json |
GET | Dynamic merged OpenAPI 3.0 spec |
v0.6.1 — Backup/Restore + Documentation
Operational tooling for data safety and extension authoring.
Added
- Backup/Restore: Full-instance backup as
.swbZIP archive containing core tables (JSONL), ext_data tables, and package assets. Stream to client or save server-side. Restore wipes and rebuilds from archive. Dialect-neutral (works across SQLite and Postgres). - Admin backup section: New "Backup" tab under
/adminwith create, download, delete, and restore operations. Destructive restore requires confirmation dialog. - Documentation surface: Builtin surface at
/docswith sidebar navigation and client-side markdown rendering. Ships with 5 documents: Getting Started, Extension Guide, API Reference, Deployment, Package Format. - Docs API:
GET /api/v1/docslists available documents,GET /api/v1/docs/:namereturns raw markdown content. Authenticated (all users). - Tests: 6 backup handler tests + E2E script (
ci/e2e-backup-test.sh).
API
| Endpoint | Method | Description |
|---|---|---|
/api/v1/admin/backup |
POST | Create backup (stream or ?store=true) |
/api/v1/admin/backups |
GET | List server-side backups |
/api/v1/admin/backups/:name |
GET | Download backup |
/api/v1/admin/backups/:name |
DELETE | Delete backup |
/api/v1/admin/restore |
POST | Restore from .swb upload |
/api/v1/docs |
GET | List documentation |
/api/v1/docs/:name |
GET | Get document content |
v0.6.0 — Cluster Registry + HA
MVP convergence point. PG-backed cluster registry for horizontal scaling — zero new infrastructure (no etcd/Consul/Redis).
Added
- Cluster registry:
node_registryUNLOGGED table with self-registration on startup (INSERT ... ON CONFLICT DO UPDATE), heartbeat tick (default 10s), stale sweep (default 30s threshold), and self-eviction (os.Exit(1)when swept by a peer — K8s restarts the process). - Cluster admin API:
GET /api/v1/admin/clusterreturns all registered nodes with runtime stats (goroutines, heap, GC, uptime, ws_clients) in the standard{data: [...]}envelope. - Health endpoint cluster info:
GET /healthandGET /api/v1/healthnow includenode_idandcluster: {size, peers, heartbeat_age_ms}when running on Postgres with the registry active. - Cluster dashboard:
cluster-dashboardadmin surface package renders one card per node with live runtime stats. Auto-refreshes every 10s. Stats are JSONB — new keys render automatically without schema migration. - Cluster config:
CLUSTER_NODE_ID(default hostname-PID),CLUSTER_HEARTBEAT_INTERVAL(default 10s),CLUSTER_STALE_THRESHOLD(default 30s),CLUSTER_ENDPOINT(Phase 2 mesh, auto-detect). - 3-replica E2E test:
docker-compose-e2e.ymlnow runs 3 replicas.ci/e2e-cluster-test.shverifies registration, stale sweep on stop, and re-registration on restart. - 5 new tests: 3 cluster registry unit tests (stats collection, start/stop lifecycle, node ID) + 2 handler tests (list nodes, empty response).
- Curated bundle expanded:
chatandcluster-dashboardadded to the default bundle (now 10 packages). Production deployments can override withBUNDLED_PACKAGES=notes,chat,chat-core,cluster-dashboardfor a lean set.
Changed
- SQLite deployments are unaffected — cluster store is nil, all cluster code
is guarded behind
database.IsPostgres() && stores.Cluster != nil. Cluster dashboard gracefully shows "0 nodes registered" on SQLite.
v0.5.5 — Upgrade Testing
Fixed
- Bundled permission auto-grant on Postgres: The
InstallBundledPackagesauto-grant SQL used SQLite-stylegranted = 1/granted = 0which fails on Postgres BOOLEAN columns. Now dialect-aware:true/falseon Postgres,1/0on SQLite. This caused bundled extensions (notes, chat-core, etc.) to install with permissions not granted on Postgres deployments. - E2E test API field corrections: Fixed
ci/e2e-chat-test.shlogin field ("username"→"login"), token extraction ("token"→"access_token"), and profile endpoint (/api/v1/me→/api/v1/profile).
Added
- Upgrade test harness:
docker-compose-upgrade.yml+ci/e2e-upgrade-test.shorchestrate a full upgrade cycle: build old image, seed data (notes, conversations, messages, settings), stop old, start new, verify data integrity post-upgrade. Covers auth, notes, conversations, packages, and settings. - Rolling upgrade test:
ci/e2e-upgrade-rolling.shtests rolling upgrade with 2 replicas sharing Postgres. Upgrades one replica at a time, verifies cross-replica reads during the mixed-version window. - 11 new Go tests in
upgrade_test.go:- Schema edge cases: add index (idempotent), add column (idempotent), multi-column add, row preservation across migration.
- Settings migration: global/team/user overrides preserved across package update, new keys get defaults, removed keys not deleted.
- Package compatibility: bundled skip-if-present on restart, dormant status for unmet requires, enabled/type preserved across version bump.
- Direct
MigrateExtTablestests: new table creation, column add with row preservation, index idempotency.
v0.5.4 — Package Updates
Added
- Package update API:
POST /api/v1/admin/packages/:id/updateaccepts a.pkgarchive, validates semver version bump (rejects same/older), applies additive schema migration, merges settings, replaces assets, and re-syncs permissions/triggers/dependencies. - Package export API:
GET /api/v1/admin/packages/:id/exportstreams the installed package as a downloadable.pkgZIP archive. Completes the manual rollback story: export before update, re-install old.pkgif needed. - Semver parsing:
ParseSemver/Compareinhandlers/semver.gowith prerelease support. Used by the update handler to enforce version bumps. - Additive schema migration:
MigrateExtTablesdiffs declareddb_tablesagainst existing ext_data schema. Adds new columns (ALTER TABLE ADD COLUMN) and new tables. No destructive changes — columns in DB but absent from manifest are preserved.ListExtColumnsintrospects viaPRAGMA table_info(SQLite) orinformation_schema(Postgres). - Settings merge on update: New manifest setting keys get their declared default value; existing admin-configured values are preserved.
- Admin UI Update button: Non-core package cards now show an Update button with a confirm dialog before upload.
- 14 new Go tests: Version bump, 5 rejection cases (same/older/core/type/id mismatch), schema add column with data preservation, schema add table, settings merge, export with valid zip, default allowlist, wildcard allowlist.
Changed
- Curated default bundle: Default bundled packages reduced from 23 to 8
core packages (notes, chat-core, workflow-chat, dashboard, 4 demo workflows).
BUNDLED_PACKAGES=*installs all; explicit comma-separated list for custom. Existing deployments are unaffected (install-once, skip-if-present). BUNDLED_PACKAGESenv var: Empty value now installs curated defaults instead of all packages. Use"*"for previous install-all behavior.
v0.5.3 — Chat Polish + Integration Testing
Added
db.query()search_like: New kernel-levelsearch_likekwarg for text search. Accepts{col: pattern}dict, generates OR-joinedLIKE(SQLite) /ILIKE(Postgres) clauses. Composes with existingfiltersvia AND. Reusable by any package. 5 new sandbox tests.- Conversation search:
GET /search?q=termendpoint inchat-core. Searches conversation titles and message content, scoped to user's conversations. Returns{conversations: [...], messages: [...]}. - Search UI: Debounced search input in chat sidebar. Results view replaces conversation list showing matched conversations and message snippets with section headers. Click result to navigate, clear button to dismiss.
workflow-chatlibrary package:on_advancehook that creates scoped group conversations when workflow stages require team collaboration. Adds all team members as participants, sends system message linking to the instance, enrichesstage_datawithconversation_id. Idempotent — skips if conversation already exists.- Multi-user E2E test infrastructure:
docker-compose-e2e.ymlwith 2 replicas, Postgres, and nginx load balancer.ci/e2e-chat-test.shshell script covering auth, conversation CRUD, message send/read, cross-replica consistency, search, and pagination.ci/e2e-ws-listener.jsNode.js WebSocket client for realtime event testing. - Workflow hook tests: 6 new Go tests for
parseOnAdvanceResultcovering None, nil, error, enriched stage_data, non-dict, and nested structure cases.
Changed
- Message pagination polish: Scroll position now preserved when loading older
messages — records
scrollHeightbefore prepend, restores viarequestAnimationFrame. Loading spinner shown at top of thread during fetch. "Load older messages" button hidden while loading. - chat-core bumped to v0.2.0 (new search endpoint).
- chat bumped to v0.2.0 (search UI, pagination polish).
v0.5.2 — Chat Surface
Added
chatsurface package: Full messaging UI built onchat-corelibrary. Route/s/chat, icon💬, depends onchat-core.- Conversation list sidebar: Conversations sorted by recent activity, last message preview, relative timestamps, unread count badges. "New" button for conversation creation.
- Message thread: Paginated message history with cursor-based load-more on scroll-to-top. Participant avatars, display names, timestamps. Scroll-to- bottom on new messages.
- Compose bar: Auto-resizing textarea, Enter-to-send, Shift+Enter for newline. Typing indicator broadcast on keypress (3s debounce).
- Participant sidebar: Collapsible right panel with participant list, online
status via presence hub, role badges. Add participant via
sw.ui.Dialog+sw.ui.UserPicker. Remove participant with confirmation. - Create conversation dialog: Group or Direct Message type selector. Multi- user picker with chip display for group, single picker for DM. Auto-title from participant names.
- Typing indicators: Realtime broadcast via
POST /typing/:idStarlark endpoint. Frontend subscribes toconversation:{id}channel, shows "X is typing..." with 4s auto-expire. Handles multiple typers. - Read receipts: Auto mark-read on conversation focus and new incoming messages. Unread counts in conversation list, cleared on selection.
- Message editing: Inline edit mode on own messages with textarea. Save on Enter, cancel on Escape. "(edited)" label on edited messages. Realtime propagation to other participants.
- Message deletion: Confirm dialog before delete. "This message was deleted" placeholder. Admin can delete any message. Realtime propagation.
Fixed
- Stale token login deadlock: REST client's 401→refresh→retry loop caused
a deadlock when the refresh endpoint itself returned 401 (stale DB). Auth
endpoints (
/auth/login,/auth/refresh,/auth/register) are now excluded from the retry loop. Defensive 8s timeout onauth.boot()ensures login page always renders even on unexpected hangs. - Bundled package permissions: Bundled packages were installed with
pending_reviewstatus andgranted=0permissions, requiring manual admin approval. Bundled installer now auto-grants all declared permissions and sets status toactiveon install. - Creator display name:
chat-core.create()now acceptscreator_display_nameso the conversation creator shows a readable name in the participant sidebar instead of a raw UUID. - Chat dark mode theming: Replaced non-existent CSS variable names with
actual theme tokens from
variables.css. Fixes invisible text and wrong backgrounds in dark mode. - Dialog dropdown clipping: UserPicker autocomplete dropdown in dialogs
(New Conversation, Add Participant) no longer clipped by
overflow: autoon.sw-dialog__body. - User menu at scaled UI: Menu primitive now divides anchor coordinates
and viewport dimensions by
sw.shell.getScale()to compensate for CSStransform: scale()on#surfaceInner. Fixes menu clipping at 110%+.
Changed
- Named Docker volume:
docker-compose.ymlswitched from bind mount (./data:/data) to named volume (sb_data:/data). Fixes volume mount failures when Docker daemon runs on a remote host (DinD, k8s pod). Deterministic wipe viadocker compose down -v.
v0.5.1 — Chat Core Library
Added
chat-corelibrary package: Conversations, messages, participants, and read cursors as ext_data tables. Permissions:db.write,realtime.publish. Consumable by other packages vialib.require("chat-core").- 4 ext_data tables:
conversations(title, type, created_by, updated_at),participants(conversation_id, participant_id, type, display_name, role, joined_at),messages(conversation_id, participant_id, content, content_type, edited_at),read_cursors(conversation_id, participant_id, last_read_message_id). - 6 exported Starlark functions:
create(),send(),history(),add_participant(),remove_participant(),mark_read(). - 15 REST endpoints: Full CRUD for conversations, messages, participants. Cursor-based paginated message history. Per-conversation unread counts.
- Realtime events:
message,message.edited,message.deleted,participant.added,participant.removedpublished toconversation:{id}channels. db.query()range parameters: Newbeforeandafterkwargs accept dicts of{col: val}generatingcol < ?/col > ?WHERE clauses. Enables cursor-based pagination and efficient unread counting for all extension packages. 7 new Go tests.
Security
- Invisible Unicode scanning gate (GlassWorm / Trojan Source defense):
ScanSource()detects variation selectors, bidi overrides, zero-width chars, tag characters, and other invisible Unicode across 10 ranges.Verdict()blocks GlassWorm payloads (>=10 variation selector / tag chars), Trojan Source (any bidi override), and excessive zero-width chars (>=6). Two enforcement points: extension install (422 rejection before files written) and Starlark execution (belt-and-suspenders). 23 new tests including GlassWorm payload simulation.
Changed
- Admin packages type filter: Added
Libraryoption alongside Surface/Extension/Full/Workflow. Filter dropdown replaced button group with themedDropdownprimitive (keyboard nav, CSS-var theming, consistent with rest of admin UI). - SDK version bumped from 0.5.0 to 0.5.1.
v0.5.0 — Realtime Primitive + Dialog Audit + Permissions UI
Added
- Realtime Starlark module:
realtime.publish(channel, event, data)lets extensions publish events to WebSocket channels. Gated by the newrealtime.publishpermission. Payloads limited to 7KB (pg_notify safe). Reserved event prefixes (system.,workflow., etc.) are blocked. - WS room protocol: Clients send
room.subscribe/room.unsubscribemessages to join/leave rooms. Intercepted in the WebSocket readPump before the bus publish gate (like ping). Per-connection cap of 100 rooms. - SDK realtime module:
sw.realtime.subscribe(channel, [event], callback)manages room join/leave over WebSocket and filters incomingrealtime.*events by room. Returns unsubscribe handle. Auto re-joins all rooms on WebSocket reconnect.sw.realtime.channels()debug helper. - Admin permissions UI: Packages page gains a "Permissions" button for packages with declared permissions. Inline drawer shows each permission with Grant/Revoke toggle, granted-by user ID, and a "Grant All" bulk action.
- Status badges:
pending_review(amber) andsuspended(red) badges on package rows. Enable toggle disabled with hint whenpending_review. - SDK API client:
permissions(),grantPerm(),revokePerm(),grantAllPerms()methods added tosw.api.admin.packages. starlarkToGoVal()helper: Converts Starlark values to Goanyfor JSON marshaling (reverse of existinggoValToStarlark).- 8 new Go tests: Route table cases for
realtime.*androom.*, realtime module publish (happy path, empty data, missing args, reserved prefix, payload too large),starlarkToGoValtype conversion.
Changed
- Dialog audit: 5 bare
confirm()calls migrated toawait sw.confirm()with{ destructive: true }styling in tasks, schedules, notes (×2), and editor packages. Package archives rebuilt. - SDK version bumped from 0.2.3 to 0.5.0.
- Event route table:
realtime.→ DirToClient,room.subscribe/room.unsubscribe→ DirFromClient. - Runner gains
busfield andSetBus()method; wired in main.go.
v0.4.7 — Note Graph
Added
- Graph API:
GET /graphendpoint returns all non-archived notes as nodes and resolved wikilinks as edges in a single payload. Nodes include id, title, folder_id, and tags. Edges include source, target, and link text. - Graph view: Canvas-based force-directed graph visualization replaces the editor pane when "Graph" button is clicked in the topbar. Force simulation uses repulsion (all pairs), attraction (edges), and center gravity with velocity damping. Supports zoom (scroll wheel) and pan (mouse drag).
- Click focus: Single-click a node to dim unconnected nodes/edges to 15% opacity; clicked node and direct neighbors stay at full brightness. Edges between focused nodes highlighted. Click empty space to reset.
- Shift+click chain: Hold shift and click a second node to union both neighborhoods — trace thought paths across two hops.
- Double-click open: Double-click a graph node to navigate to that note in the editor. Graph view closes, editor opens with the selected note.
- Hover tooltips: Hovering a node shows title, tag pills, and connection count in an HTML overlay (no additional API calls).
- Folder coloring: Nodes colored by folder using a 10-color palette. Unfiled notes shown in gray.
- Tag filter integration: Active tag filter dims non-matching nodes to 30% opacity, stacking with click focus.
- Orphan highlighting: Notes with zero connections shown with dashed stroke. "Hide orphans" checkbox in toolbar to toggle visibility.
Changed
- Notes package version bumped from 0.7.0 to 0.8.0.
- Topbar gains a "Graph" toggle button alongside "New Note" and "Import .md".
v0.4.6 — Sidebar Restructure
Added
- Sidebar tabs: Two-tab header ("Notes" / "Outline") replaces the static sidebar header. Notes tab contains folder tree, tag filter, search, and note list. Outline tab shows the document heading tree for the active note. Outline tab disabled (grayed) when no note is selected.
- Sidebar outline: Document outline relocated from editor-side panel into
the sidebar's Outline tab. Nested heading hierarchy (H1→H2→H3) with depth
indentation via
padding-left. Click heading scrolls to it in both preview and CM6 editor modes. - Active heading tracking: Scroll listener (RAF-throttled) highlights the
current heading in the sidebar outline based on scroll position. Uses
getBoundingClientRect()in preview/split mode and CM6 viewport line in edit mode.
Changed
- Notes package version bumped from 0.6.0 to 0.7.0.
EditorPaneexposes headings, scroll function, and active heading index to parent via callback props (onHeadingsChange,scrollToHeadingRef,onActiveHeadingChange).- Sidebar auto-resets to Notes tab when active note is deleted.
Removed
- Old
DocumentOutlinecomponent and.doc-outlineCSS styles (replaced by sidebar outline).
v0.4.5 — Editor Modes + Document Outline
Added
- Tri-state view mode: Notes open in rendered (read-only) mode by default. Toolbar button cycles through Edit (CM6) and Split (side-by-side) modes. Replaces the old binary preview toggle.
- Split view: Side-by-side CM6 editor + rendered preview using CSS grid. CM6 instance preserved when toggling between edit and split modes.
- Synced scroll: In split mode, scrolling the CM6 editor proportionally
scrolls the preview pane via linear ratio mapping on
scrollDOM. - View mode persistence: User's preferred mode saved to localStorage
(
sw.storage.local('notes')). Neweditor_modemanifest setting with admin-level default (rendered / edit / split). - Document outline: Collapsible TOC panel adjacent to the editor body.
parseHeadings()extracts heading level, text, and line number (skips fenced code blocks). Click heading to scroll — uses CM6EditorView.scrollIntoView()in edit/split mode, DOMscrollIntoView()in rendered mode. Updates on body change with 300ms debounce.
Changed
- Notes package version bumped from 0.5.0 to 0.6.0.
- Editor body wrapped in
.notes-editor__contentflex container to accommodate the outline panel. - Wikilink click handling active in both rendered and split modes.
- Mobile: split view collapses to single column, outline panel hidden.
v0.4.4 — Rich Editor + Import/Export
Added
- CodeMirror 6 integration: Notes editor now uses the vendored CM6 bundle
(
CM.noteEditor()) for rich markdown editing with syntax highlighting, wikilink autocomplete ([[triggers note title completion), and inline preview decorations for headings, code blocks, and blockquotes. - CM6 dynamic loading: Bundle loaded via
<script>tag at boot time. Falls back to plain textarea if CM6 is unavailable. - Export as .md: Export button in editor header downloads the note as a Markdown file with YAML frontmatter (title, tags, created date).
- Import .md: Import button in topbar allows uploading
.md/.markdown/.txtfiles. Parses YAML frontmatter for title and tags, creates note via API. - CSS for CM6: Editor fills the container with
max-height: noneoverride and proper scroller padding.
Changed
- Notes package version bumped from 0.4.0 to 0.5.0.
EditorPanecomponent refactored to support CM6 with mutable refs for title/body (required for CM6 callback closures).- Ctrl/Cmd+S save works in both CM6 and textarea fallback modes.
v0.4.3 — Backlinks + Wikilinks
Added
- Links table (
ext_notes_links): source_id, target_id, link_text columns with indexes on source_id and target_id for bidirectional lookup. - Wikilink extraction:
_extract_wikilinks()parses[[...]]patterns usingsplit("[[")+find("]]")(Starlark has no regex or while loops). Returns deduplicated list of link text strings. - Link sync on save:
_sync_links()called from_create_noteand_update_notewhen body changes. Follows the tags delete-all + reinsert pattern. Resolves link text to note IDs via case-insensitive title matching. Unresolved links stored with emptytarget_id. - Cascade delete: Hard-deleting a note removes both outgoing links (source_id matches) and incoming backlinks (target_id matches).
- Link API endpoints:
GET /links/:note_idreturns outgoing links.GET /backlinks/:note_idreturns incoming links enriched with source note titles to avoid extra frontend round-trips. - Wikilink preview rendering:
[[Note Title]]syntax renders as clickable accent-colored links in the markdown preview. Unresolved wikilinks styled in danger/red with dashed underline. - Wikilink click navigation: Clicking a resolved wikilink navigates to the target note. Clicking an unresolved wikilink creates the note and navigates to it, then re-saves the source note so the link resolves to blue.
- Backlinks panel: Collapsible panel below the editor showing all notes that link to the current note. Each backlink displays source title and link text. Click to navigate. Hidden when no backlinks exist.
- Updated stats:
/statsresponse now includeslinkscount.
Data model
| Table | Columns |
|---|---|
ext_notes_links |
source_id, target_id, link_text |
Notes package version
0.3.0 → 0.4.0
v0.4.2 — Tags + Search
Added
- Tags table (
ext_notes_tags): note_id, tag columns with indexes on both for bidirectional lookup. Many-to-many relationship via join table. - Tag CRUD API: 3 new Starlark handlers — list all unique tags, get tags for a note, replace tags for a note (delete-all + reinsert). Tags are normalized to lowercase, trimmed, and deduplicated on save.
- Tags in list/get/search:
_list_notesbatch-fetches all tags in a single query and attaches atagsarray to each note item._get_noteincludes tags._search_notesnow matches search terms against tags. - Tag cascade delete: Hard-deleting a note also removes its tag rows.
- Tag input in editor:
TagInputcomponent with removable pills, text input (comma or Enter to add), and autocomplete dropdown populated from all existing tags. - Tag pills on note cards:
NoteCardrenders up to 3 tag pills with "+N" overflow indicator for notes with many tags. - Tag filter in sidebar:
TagFiltercomponent displays all unique tags as clickable pills. Clicking toggles client-side filtering of the note list by that tag. - Drag-and-drop note moves:
NoteCardis draggable (HTML5 drag/drop).FolderNode, "All Notes", and "Unfiled" items are drop targets. On drop, calls the existing move-note API. - Folder context menu: Replaced
prompt()hack with a proper right-click popup menu showing "Add subfolder", "Rename", and "Delete" actions. Positioned at click coordinates, dismissed on outside click. - Updated stats:
/statsresponse now includestagscount (unique).
Data model
| Table | Columns |
|---|---|
ext_notes_tags |
note_id, tag |
Notes package version
Bumped from 0.2.0 → 0.3.0. 15 API routes (12 existing + 3 new).
v0.4.1 — Folders + Navigation Tree
Added
- Folders table (
ext_notes_folders): name, parent_id, creator_id, sort_order. Indexed on parent_id and creator_id. Nested hierarchy via parent_id foreign key (empty string = root). - Folder CRUD API: 5 new Starlark handlers — list, create, update, delete
folders plus a dedicated move-note endpoint (
POST /notes/move). - Navigation tree:
FolderTree+FolderNodePreact components in the sidebar. Flat API response built into a nested tree client-side viauseMemo. Expand/collapse toggles, depth-based indentation, inline rename via right-click context menu. - Folder filtering: Clicking a folder filters notes by
folder_idquery param. "All Notes" clears the filter. "Unfiled" shows notes with empty folder_id (client-side filter). - Editor folder select: Dropdown in the editor header moves notes between
folders. Shows nested hierarchy with
└prefix indentation. - Folder-aware note creation: New notes inherit the active folder's ID.
- Delete cascade: Deleting a folder orphans its notes (moves to Unfiled) and reparents child folders to the deleted folder's parent.
- Updated stats:
/statsresponse now includesunfiledandfolderscounts.
Data model
| Table | Columns |
|---|---|
ext_notes_folders |
name, parent_id, creator_id, sort_order |
Notes package version
Bumped from 0.1.0 → 0.2.0. 12 API routes (7 existing + 5 new).
v0.4.0 — Notes Surface
Added
- Notes surface package (
packages/notes/): Obsidian-style markdown notes rebuilt as a standard installable.pkgarchive. Typefull, tierstarlark. Zero kernel changes — proves the extension stack end-to-end. - Starlark backend (
script.star): 7 API routes — CRUD, search, stats. Note bodies stored inext_notes_notesTEXT columns. List endpoint returns lightweight projections (no body); full content fetched on select. - Markdown editor: Preact+htm frontend with sidebar note list, monospace textarea, and toggle-able live preview. Inline markdown renderer (~100 lines) covers headings, bold, italic, code blocks, links, lists, blockquotes, and HR.
- Auto-save: Debounced save (1s) with dirty/saved status indicator.
Ctrl/Cmd+Sfor force save. Tab key inserts two spaces. - Pin & archive: Pin important notes to the top of the list. Archive
(soft-delete) via the delete button; hard delete with
?hard=1query param. - Search: Case-insensitive full-text search across title and body content. Search bar in sidebar filters the note list in real-time.
- Theme support: All styles use CSS variables — light and dark themes work out of the box. Responsive layout collapses to vertical on narrow viewports.
Data model
| Table | Columns |
|---|---|
ext_notes_notes |
title, body, folder_id, creator_id, updated_at, pinned, archived |
Indexes on folder_id, creator_id, pinned, updated_at.
Planned (v0.4.1–v0.4.4)
- Folders + navigation tree
- Tags + enhanced search
- Backlinks +
[[wikilinks]] - Rich editor (CodeMirror 6) + markdown import/export
v0.3.8 — Distribution
Added
- Bundled packages: Production Docker image now ships with 12 pre-built
.pkgarchives (4 workflows, 3 surfaces, 1 full, 1 library, 2 test runners, 1 extension). Auto-installed on first boot viaInstallBundledPackages(). Install-once, skip-if-present — admin uninstalls are respected on restart. - Package allowlist (
BUNDLED_PACKAGES): Comma-separated list of package IDs to install. Empty (default) installs all. Useful for Helm/K8s where different environments need different packages. - Skip bundled (
SKIP_BUNDLED_PACKAGES=true): Disables auto-install entirely. Intended for production until post-MVP packages are ready. - Builder image (
Dockerfile.builder): Pre-caches Go modules, Node dependencies, and vendor lib tarballs for faster custom builds. - Distribution docs (
docs/DISTRIBUTION.md): Quick start, bundled packages, builder image usage, custom build guide, production deployment reference. - Migration 012: Adds
bundledtopackages.sourceCHECK constraint (both Postgres and SQLite). - K8s manifest:
SKIP_BUNDLED_PACKAGESandBUNDLED_PACKAGESenv vars added tok8s/armature.yaml. - Tests: 6 handler tests (fresh install, skip existing, missing dir, empty dir, dormant handling, allowlist filtering).
Environment defaults
| Environment | SKIP_BUNDLED_PACKAGES |
BUNDLED_PACKAGES |
|---|---|---|
| Dev (compose) | false |
(empty = all) |
| Test (K8s) | false |
(empty = all) |
| Prod (K8s) | true |
— |
v0.3.7 — Package Audit
Added
- Dormant status: New
dormantvalue inpackages.statusCHECK constraint (both Postgres and SQLite). Installer auto-detects packages with unmetrequiresentries and setsstatus=dormant, enabled=false. Enable endpoint returns 409 for dormant packages. - Generalized requires check: Any unmet requirement triggers dormant status, not just specific values. Known capabilities checked against a whitelist; anything unknown is unmet.
- Admin UI indicators: Dormant badge, disabled Enable button with tooltip, Dormant stat card in packages admin view.
- Tests: 4 new handler tests covering dormant status, enable blocking, surface exclusion, and admin list inclusion.
Fixed
- Manifest fixes: Fixed 6 chat-extension manifests (
"name"→"title") that were blocking install. Added explicit"type": "surface"to hello-dashboard, icd-test-runner, sdk-test-runner. - Dashboard + Editor tagged dormant: Both depend on removed
sw.*imperative SDK (sw.tabs,sw.chat,sw.layout, etc.) — render blank. Tagged withrequires: ["legacy-sdk"]so installer auto-sets dormant. - Dependency check: Relaxed library dependency validation to allow
pending_reviewlibraries (gitea-client declares permissions). Onlysuspended/dormantlibraries blocked.
Package Audit Results
- 7 working surfaces: schedules, tasks, team-activity-log, git-board, hello-dashboard, icd-test-runner, sdk-test-runner
- 8 dormant: 6 chat-dependent (csv-table, diff-viewer, js-sandbox, katex-renderer, mermaid-renderer, regex-tester) + 2 legacy-sdk (dashboard, editor)
- 1 library: gitea-client (standalone, active)
v0.3.6 — Example Workflows + Interactive Demo
Added
- Bug Report Triage (
packages/bug-report-triage): Public entry, progressive fieldsets, severity-based branch routing, SLA timer (3600s on critical fix). - Employee Onboarding (
packages/employee-onboarding): Starlark automated stages (db.insert,notifications.send), manager signoff gate withrequired_role, rejection reroute.script.starwithon_provisionandon_welcomehooks. Createsprovisionsandonboarding_logext_data tables. - Content Approval (
packages/content-approval): Multi-party signoff (quorum of 2), rejection reroute creating a review cycle loop. - Webhook Notifier (
packages/webhook-notifier): Starlarkhttp.postwithconnections.getfallback, delivery logging towebhook_logext_data table. - Workflow Demo surface (
packages/workflow-demo): Interactive walkthrough with workflow cards, stage flow diagrams, Starlark viewer, API curl examples, and "Try It" buttons. Route:/s/workflow-demo. - Engine context fix:
started_byadded to automated stage Starlark context dict, enabling hooks to reference the workflow initiator. - SLA package installer:
sla_secondsfield added toworkflowPkgStagestruct and wired in both install and export paths. - Snapshot format fix:
parseSnapshotStageshelper handles both wrapped ({"stages":[...]}) and legacy flat array snapshot formats in the engine, fixingcorrupt version snapshoterrors when starting published workflows. - Workflow adoption:
POST /teams/:teamId/workflows/:id/adoptclaims a global (team_id=NULL) workflow for a team.GET /teams/:teamId/workflows/availablelists adoptable workflows.TeamIDadded toWorkflowPatchmodel. - Tests: 3 new engine tests (automated context, SLA install, manifest roundtrip). Total: 142 handler tests passing.
- Per-package
README.mdfiles for all 5 new packages.
Fixed
- Demo surface: Added
sw.userMenuto topbar. Fixed workflow install detection (was reading wrong response key). CSS uses theme variables for dark mode support. - Admin teams tab: Extracted
.datafrom paginated response — was stuck at loading becausesetTeamsreceived the envelope object, not the array. - Team-admin workflows: Added "Adopt Global" button to claim package-installed workflows into a team scope. Workflow list now unwraps paginated responses.
v0.3.5 — Settings Audit + ICD + Tests + Clone
Added
- Clone endpoint:
POST /api/v1/workflows/:id/clonedeep-copies a workflow and all its stages. Creates an inactive draft with "Copy of" prefix. Handles slug collisions with auto-suffix. Team-scoped mirror at/api/v1/teams/:teamId/workflows/:id/clone. - Integration tests: 7 new engine-level tests in
workflow_engine_test.gocovering full lifecycle, branch routing, public entry, signoff validation gate, signoff rejection, cancel-clears-assignments, and error cases. Total: 28 tests. - Staleness timeout UI:
staleness_timeout_hoursinput added to workflow editor in team-admin settings (was backend-only since v0.3.3). - Branch rules UI: Collapsible JSON textarea in stage form for configuring conditional routing rules (was backend-only since v0.3.2).
Changed
- ICD (OpenAPI spec): Fixed stale
Workflowschema (addeddescription,branding,is_active,on_complete,retention,staleness_timeout_hours). Fixed staleStageschema (removedhistory_mode/transition_rules, updatedstage_modeenum to[form, review, delegated, automated], addedaudience,stage_type,starlark_hook,branch_rules,stage_config,sla_seconds,auto_transition,assignment_team_id,surface_pkg_id). AddedWorkflowInstance,WorkflowAssignment,WorkflowSignoffschemas. Added ~20 new endpoint paths: instances, assignments, signoffs, public entry, clone, and team roles. Added tags: Workflow Instances, Workflow Assignments, Workflow Signoffs, Public Workflows.
v0.3.4 — Team Roles + Multi-party Validation
Added
- Custom team roles: Removed
CHECK (role IN ('admin','member'))constraint fromteam_members(both Postgres and SQLite). Custom roles are stored inteams.settings["roles"]as a JSON array. Builtinsadminandmemberare always present. - Team roles API:
GET /api/v1/teams/:teamId/rolesreturns configured roles.PUT /api/v1/teams/:teamId/rolesreplaces the roles array (builtins enforced). - Role-based stage assignment:
stage_config.required_rolerestricts which team members can claim an assignment.CheckClaimRoleverifies the user's team membership role before allowing claim; rolls back on mismatch (403). - Multi-party sign-off: New
workflow_signoffstable withUNIQUE(instance_id, stage, user_id)preventing double-signing.StageConfig.validationsupportsrequired_approvals,required_role, andreject_action(cancel or reroute to named stage). - Validation gate:
advanceInternalblocks stage advancement until the required number of approvals is met. Rejections trigger cancel or reroute based onreject_action. Engine.SubmitSignoff: Records approval/rejection, enforces signoff role if configured, emitsworkflow.signoffevent.- Signoff HTTP API:
POST /api/v1/instances/:iid/signoffs(submit),GET /api/v1/instances/:iid/signoffs(list current stage signoffs). Team-scoped mirrors at/api/v1/teams/:teamId/instances/:iid/signoffs. - New events:
workflow.signoff,workflow.rejected. - Frontend — Members page: Dynamic role selects populated from team roles API. "Manage Roles" panel for adding/removing custom roles inline.
- Frontend — Stage editor: "Required Role (claim)" dropdown and "Multi-party Validation" section (required approvals, signoff role, on reject action) appear when a team is selected for the stage.
- Frontend — Monitor tab: Signoff panel with approve/reject buttons, comment field, and approval progress display.
- 3 new store tests: CreateSignoff (with UNIQUE constraint check), ListSignoffs (ordering), CountSignoffs (decision filter). 20 total, all passing.
- Design doc:
docs/DESIGN-EXTENSION-LIFECYCLE.md— permanent vs PoC packages, graduation criteria, explicit install model. - Design doc:
docs/DESIGN-TRIGGER-COMPOSITION.md— triggers/schedules can start workflows, workflows emit events, no circular invocation.
Changed
addMemberRequestandupdateMemberRequestbinding relaxed fromoneof=admin membertorequired,min=1,max=50with application-level validation against the team's configured roles.TruncateAlltest helper updated to includeworkflow_signoffs.
v0.3.3 — Public Entry + Background Jobs
Added
- Public workflow entry: Unauthenticated routes for anonymous workflow
participation.
POST /api/v1/public/workflows/:id/startcreates an instance withstarted_by = "public:<uuid>"and returns an entry token.GET .../resume/:tokenandPOST .../advance/:tokenallow continuation. Only stages withaudience = "public"can be advanced anonymously. - SLA scanner: Background goroutine (5-minute interval) checks active
instances against per-stage
sla_seconds. Firesworkflow.sla_breachevent on first breach, markssla_breached: truein instance metadata (idempotent). - Staleness sweep: New
staleness_timeout_hourscolumn onworkflows. Scanner marks instances asstalewhenupdated_atexceeds the threshold, cancels open assignments, firesworkflow.staleevent. - New store methods:
ListActiveInstances(),MarkInstanceStale(). - New events:
workflow.sla_breach,workflow.stale. - 3 new store tests: MarkStale, ListActive, StalenessTimeoutHours round-trip.
v0.3.2 — Workflow Engine
Added
- Stage execution engine:
server/workflow/engine.go—Start,Advance(withbranch_rulesconditional routing), andCanceloperations. Mergesstage_dataacross stages, creates assignments, emits lifecycle events. - Automated stage handler:
server/workflow/automated.go— fires Starlark hook, auto-advances on success, cycle guard (max 10 consecutive automated stages). - Instance HTTP API: Start, GetInstance, Advance, Cancel, ListInstances.
Team-scoped mirrors under
/api/v1/teams/:teamId/workflows/. - Assignment HTTP API: Claim, Unclaim, Complete, Cancel, ListByTeam, ListMine. Claimer identity verification on unclaim/complete.
- Starlark module expansion:
workflow.get_instance(),workflow.list_instances()(read-only). - 14 store tests covering all 15 v0.3.1 methods (instance + assignment CRUD). All passing.
v0.3.1 — Instance Assignment
Added
workflow_instancestable: Tracks workflow execution state —workflow_version,current_stage,stage_data,status,entry_token.workflow_assignmentstable: Per-stage queue for instance assignments —instance_id,stage,team_id,assigned_to,status,review_data. Optimistic claim locking via status transition.- 15 new store methods: Full CRUD for instances (Create, Get, GetByToken, Update, List, AdvanceStage, Complete, Cancel) and assignments (Create, Claim, Unclaim, Complete, Cancel, ListByTeam, ListByInstance, ListByUser).
- New events:
workflow.started,workflow.cancelled,workflow.error.
v0.3.0 — Workflow Schema Redesign
Changed
workflow_stagesschema modernized: Dropped chat-era columns (persona_id,history_mode). Renamedtransition_rules→stage_config. Updatedstage_modeCHECK from(form_only, form_chat, review, custom)to(form, review, delegated, automated).- New stage fields: Added
audience(team | public | system),stage_type(simple | dynamic | automated),starlark_hook(package_id:entry_point), andbranch_rules(JSONB array of routing conditions). - Routing engine:
ResolveNextStagenow readsbranch_rules(flat array) instead oftransition_rules.conditions(nested object). Cleaner separation between routing rules and stage config. - Hook handler:
FireOnAdvanceHookreads fromstage_configinstead oftransition_rules. RenamedchannelIDparameter toinstanceID. - Stage CRUD validation: New fields validated on create/update.
delegatedmode requiressurface_pkg_id.dynamic/automatedstage_type requiresstarlark_hook. - Package export/import: Updated to use new field names. Workflow packages
now include
audience,stage_type,starlark_hook,branch_rules,stage_config. - Starlark workflow module: Stage dicts now include
audienceandstage_typekeys. Removedhistory_modeandpersona_id. - Frontend stage editor: Both admin and team-admin surfaces updated with
new mode values, audience selector, stage type selector, and conditional
Starlark hook input. Fixed pre-existing bug in admin workflows.js where
STAGE_MODESstill containedchat_only.
Removed
persona_idcolumn fromworkflow_stages(personas are extension concerns)history_modecolumn fromworkflow_stages(chat-era context management)StageModeCustom,StageModeFormOnly,StageModeFormChatGo constants
v0.2.9 — Builtin Extension Retirement
Changed
- 6 builtin extensions → standard packages: csv-table, diff-viewer,
js-sandbox, katex-renderer, mermaid-renderer, and regex-tester moved from
extensions/builtin/topackages/with standardjs/layout. Each manifest now declares"requires": ["chat"]and"type": "extension". Built viabuild.shlike all other packages — no auto-install.
Removed
SeedBuiltinPackagesseeder: Deletedseed_packages.go(function,builtinManifeststruct,buildFullManifesthelper) and the startup call inmain.go. Extensions are no longer auto-seeded into the DB.extensions/builtin/directory: Removed from repo and Dockerfile COPY.- Seed tests: Removed 8
TestSeed_*functions andmakeSeedDirhelper fromextension_test.go(~220 lines). - Stale comments: Removed
SeedBuiltinPackagesreferences frompackage_iface.goandtrigger_sync.go.
v0.2.8 — Team Admin Settings Audit (Pass 1)
Removed
- Dead
HasPrivateProviderRequirement: Store method checked team settings forrequire_private_providers(BYOK vestige). Removed from interface, PostgreSQL, and SQLite implementations. No callers existed. - Dead
UserRolefield onTeamMember: Joinedusers.rolecolumn (deprecated in v0.2.0 RBAC migration). Removed from model,ListMembersandGetMemberqueries in both stores. Frontend never consumed it. - Dead
allow_team_providerspolicy: Removed fromPolicyDefaults, both test seed data blocks, and ICD test assertions. No handler or UI read it. - Dead personas in workflow stage UI: Removed
sw.api.teams.personas()call (endpoint doesn't exist), personas state, persona dropdown in StageForm, and persona badge in stage list. - Dead
history_modein stage UI: Removed history mode selector and state from StageForm. Backend column retained for v0.3.x schema migration. - Dead ICD tests: Removed assertions for
/teams/:teamId/personas,/teams/:teamId/providers, and/teams/:teamId/models— endpoints no longer exist in the kernel. - Stale
chat_onlystage mode: FrontendSTAGE_MODESupdated from['chat_only', 'form_only', 'form_chat', 'review']to['form_only', 'form_chat', 'review', 'custom']matching the backend CHECK constraint. - Stale comments: Removed references to deleted
team_providers.go,personas.go, andapiconfigs.gofiles.
v0.2.7 — User Settings Audit
Changed
- localStorage namespace: Renamed
cs-appearancekey tosb-appearanceacross appearance settings, base template, and workflow template. One-time migration preserves existing user preferences. - Policy-gating tests: Replaced stale
allow_user_byok/allow_user_personasassertions withallow_registrationcheck (the only policy still in admin UI).
Removed
- Dead BYOK nav section in user settings: empty
BYOK_ITEMSarray,byokEnabledstate, "BYOK Enabled" footer badge, and the nav group that rendered an empty section. - Dead personas gate:
personasEnabledstate and.filter()on NAV_ITEMS for agateproperty no items have.auth.permissions.changedlistener removed (existed solely for BYOK + personas state). - Dead Message Font Size: Slider,
msgFontstate, and--msg-fontCSS variable application from appearance section and base template. - Dead policy defaults:
allow_user_byokandallow_user_personasremoved fromPolicyDefaults, profile bootstrap, permissions handler, andPublicSettings. Test seed data cleaned.
v0.2.6 — Admin Settings Audit
Changed
- Roadmap reorder: Extension Lifecycle moved from v0.2.6 to v0.3.x (Workflows series). Settings audit milestones renumbered: v0.2.7→v0.2.6, v0.2.8→v0.2.7, v0.2.9→v0.2.8. Added v0.2.9 for builtin extension retirement (chat-centric extensions dormant until chat surface ships).
Removed
- Dead admin section categories in
sectionCategory(): AI (providers, models, personas, roles, knowledgeBases, memory), routing (health, routing, capabilities), and channels. Default category changed fromaitosystem. - Dead PublicSettings fields:
system_prompt/has_admin_prompt,retention_ttl_days,paste_to_file_chars,allow_user_personaspolicy — all chat-era with no frontend consumers. - Dead PolicyDefaults:
allow_raw_model_access,default_model. - Dead policy lookups:
allow_raw_model_accessandkb_direct_accessfrom profile bootstrap and permissions handlers. - Dead test seed data:
model_rolesglobal setting,allow_raw_model_accesspolicy from test helper. - Dead CORE_IDS entry: Removed
chatfrom packages admin page.
[Unreleased] — v0.2.5
Added
- Welcome surface: New core surface shown as fallback. Detects whether extensions exist (shows "Set Default Surface") vs truly empty install (shows "Go to Packages"). Topbar + UserMenu included.
- User default surface: Users can set a personal landing page in Settings > General, overriding the admin-configured global default.
- UserMenu in admin topbar: Replaced Back button with UserMenu, providing consistent surface navigation and eliminating infinite loops.
- Package manifest icons: Added emoji icons to hello-dashboard, icd-test-runner, sdk-test-runner, and team-activity-log manifests.
- PoC documentation: README.md for tasks and schedules packages documenting Proof of Concept status and graduation criteria.
Changed
- Default surface resolution: Priority chain is now
user preference → global config → first extension →
/welcome. User preference read from JWT cookie on unauthenticated/route. Correctly resolvestype: "full"extension packages (not justtype: "surface"). - User settings General section: Replaced dead chat fields (Default Model, System Prompt, Max Tokens, Temperature, Show Thinking) with a Default Surface dropdown.
- Admin settings: Removed dead chat sections (System Prompt, Default Model, Policies, Web Search, Auto-Compaction, Memory Extraction).
- Roadmap restructured: Workflows → v0.3.x (includes team roles), Notes → v0.4.0, MVP → v0.5.0. Added settings audit milestones (v0.2.7 admin, v0.2.8 user, v0.2.9 team-admin pass 1).
- Updated bus doc examples and test labels from
chat.*toworkflow.*. - Renamed
channel-prefixed test paths totest-in storage tests. - Updated doc comments throughout to reflect current surface architecture.
Removed
- ~500 lines of dead CSS: Orphaned classes for removed legacy UI
components (chat, channel, project, notes, editor-chat, sidebar,
router-picker) from
layout.cssandsurfaces.css. - Dead Go types:
Grantstruct (persona-era),CompositeModelKeyfunc, comment-only stubs for NoteGraph, ProjectChannel, etc. - Dead event code:
chat.typing.*/channel.typing.*condition in WS subscriber, empty Chat/Channel event route table sections. - Dead test helpers:
seed_helpers.go(SeedTestMessage, SeedTestMessages, SeedTestCursor — all callerless). - Stale template refs: CSS link tags for non-existent
sw-chat-pane.css,sw-notes-pane.css,chat.css. Orphanedchat-pane.htmlcomponent. - Vestigial guards:
"chat"surface checks inIsSurfaceEnabled()andDisablePackage()(chat is no longer a surface). - Dead settings UI: Chat defaults from user settings, System Prompt / Default Model / Policies / Web Search / Compaction / Memory from admin.
- Unused
fmt.Sprintfreferences in team stores.
[Unreleased] — v0.2.4
Added
sw.shell.Topbar: Standard navigation bar component for surfaces. Composes title + extension slot + NotificationBell + UserMenu into a consistent 44px bar. Surfaces use<${sw.shell.Topbar} title="...">with children rendered in the extension slot. Graceful fallback if unavailable.- Schedules surface (
packages/schedules/): New package wrapping the kernel/api/v1/schedulesAPI. Table view with cron badge + human-readable preview, next fire time, enable/disable toggle, manual run, and execution logs panel. Create/edit dialog with live cron-to-english preview. - Manifest
iconfield: Packages can declare an emoji icon inmanifest.json("icon": "⏰"). Served via the surfaces APIiconfield. Rendered in the UserMenu flyout next to each surface name.
Changed
- UserMenu: Surface list now driven entirely by the
/api/v1/surfacesAPI. Removed hardcoded surface links; menu is now fully dynamic. Core surfaces (Admin, Settings, Team Admin, Workflow) filtered from the API list and handled as dedicated menu items with RBAC gating. - Tasks surface: Replaced custom
.tasks-headerwithsw.shell.Topbar. View tabs and create button rendered in the extension slot.
Fixed
sw.isAdminRBAC regression:isAdmin()incan.jswas checking the deprecateduser.role === 'admin'field instead of the v0.2.0 RBAC grantsurface.admin.access. Admin menu item and admin-gated features now appear correctly for users in the Admins group.
[Unreleased] — v0.2.2
Added
- Event bus subscriptions: Extensions declare event triggers in manifest
(
"triggers": [{"type": "event", "pattern": "workflow.completed", ...}]). Wired viabus.Subscribe()on startup. Handlers fire asynchronously. - Webhook triggers: Inbound HTTP at
/api/v1/hooks/:package_id/:slug. HMAC-SHA256 verification viaX-Armature-Signatureheader. Synchronous Starlark handler can return custom HTTP status and body. - Scheduled tasks: User-created cron-scheduled Starlark scripts with restricted sandbox (no raw HTTP, no DB table creation, connections-only outbound). Runs as creator identity with RBAC scoping. Admin-created tasks can opt into system context. Creator deactivation auto-pauses schedule.
- Schedule templates: Extensions ship pre-built schedule templates in
manifest (
schedule_templatesarray) with configurable params and default cron expressions. triggers.registerextension permission — required for event/webhook triggerstriggerstable — extension-declared event and webhook trigger definitionsscheduled_taskstable — user-created cron tasks with script, template, and identity fieldstrigger_logstable — unified execution audit log for both tiersTriggerStore+ScheduledTaskStoreinterfaces (postgres + sqlite)- Trigger engine (
server/triggers/) — orchestrates event subscriptions, webhook resolution, and cron scheduling viarobfig/cron/v3 SyncManifestTriggers()— declarative sync of event/webhook triggers from manifest. Hooked into seed, admin install, and package install flows.- Admin trigger API:
GET/PUT/DELETE /admin/triggers,/admin/triggers/:id/logs,/admin/packages/:id/triggers - Admin schedule API:
GET /admin/schedules, enable/disable/delete - User schedule API: full CRUD at
/api/v1/schedules, manual run, execution logs trigger.firedandtrigger.errorevent bus labels (DirLocal) for observability- OpenAPI spec: Trigger, ScheduledTask, TriggerLog schemas + all new endpoints
[v0.2.1] — 2026-03-26
Added
- Default surface routing:
/redirects to configurable default surface. Fallback chain: configured default → first enabled extension surface →/admin. First installed extension surface auto-becomes default. Admin can change via Settings > Default Surface dropdown. default_surfaceglobal config key (JSON{"id": "slug"})- Admin settings UI: Default Surface dropdown (extension surfaces only)
- ICD (API contract): Full OpenAPI 3.0.3 spec covering all 160 kernel
endpoints. 22 tag groups (System, Auth, Profile, Workflows, Packages,
Connections, Teams, Groups, Extensions, and Admin subsections). Reusable
component schemas for User, Team, Group, Workflow, Package, Extension, etc.
Served at
/api/docs(Swagger UI) and/api/docs/openapi.yaml.
Changed
disabledRedirect()now redirects to/admininstead of/to prevent redirect loops when the default surface is disabled- Disabled extension surfaces (
/s/:slug) redirect to/admininstead of/
Fixed
- Gin route param conflict causing backend startup hang: team-scoped
package settings routes used
:pkgIdwhile sibling routes used:id. Gin's radix tree entered an infinite loop on the conflicting param names. Unified to:idacross all/teams/:teamId/packages/routes. - Docker entrypoint: increased health check timeout (10s → 60s), added stale process cleanup and crash detection to prevent zombie backends on restart
[v0.2.0] — 2026-03-26
Added
- Full RBAC: all authorization flows through group membership and permission grants. No magic roles, no implicit group membership, no special-casing.
surface.admin.accesspermission — any group can grant admin panel access- Admins system group seeded with all platform permissions
- Everyone system group — all users explicitly added on creation
EnsureEveryoneGroup(),AddToAdminsGroup(),RemoveFromAdminsGroup()helpersSeedAdminsGroupMember(),SeedEveryoneGroupMember()test helpers- System groups re-seeded after
TruncateAllin test helper - OIDC
isIdPAdmin()— maps IdP role claims to Admins group membership - Settings cascade: three-tier resolution (global → team → user) with
user_overridableflag per manifest setting key. Admins can lock settings that team admins and users cannot override. package_team_settingstable for team-scoped package setting overrides- Team admin API:
GET/PUT/DELETE /api/v1/teams/:teamId/packages/:pkgId/settings RunContext.TeamIDfor team-aware Starlark settings resolutionstore.ResolveSettings()/store.FilterOverridableKeys()pure functionsstore.ParseSettingsSchema()extractsuser_overridablefrom manifests
Changed
RequireAdmin()/RequireAdminPage()checksurface.admin.accessgrantRequirePermission()no longer bypasses for admin roleResolvePermissions()unions explicit group memberships only (no implicit Everyone)- All user creation paths (builtin, OIDC, mTLS, admin, bootstrap, seed) add to Everyone group. Admin users added to Admins group.
- JWT claims no longer include
rolefield - Login response no longer includes
rolein user object - Profile endpoint no longer returns
role - Profile bootstrap resolves permissions from groups (no admin shortcut)
- Middleware auth cache tracks
isActiveonly (no role) - Admin create user accepts
is_adminbool (not role string) - Admin update role endpoint accepts
is_adminbool, manages Admins group directly - Demotion/deletion safeguards check Admins group member count
- Notifications
RoleFallbackHandlerqueries Admins group members - OIDC syncs Admins group on login (no role column writes)
- Kernel permissions: 6 → 7 (added
surface.admin.access) - Admin users UI: role dropdown removed, admin managed through groups
- Starlark
settings.get()uses cascade resolver instead of naive merge - User settings save (
POST /extensions/:id/settings) strips non-overridable keys
Removed
users.rolecolumn — dropped from schema, model, JWT, all handlersUserRoleAdmin,UserRoleUserconstantsCountByRole()store methodDefaultRoleconfig for OIDC and mTLS providersSyncAdminsGroupMembership()(replaced byAddToAdminsGroup/RemoveFromAdminsGroup)- OIDC
resolveRole()(replaced byisIdPAdmin()) - Token budgets from groups: columns,
ResolveTokenBudget(), all store/handler/UI - Allowed models from groups: column,
ResolveModelAllowlist(), UI - Admin groups UI: Token Budgets section, Allowed Models section
Migration notes
- 001_core.sql (both dialects): removed
rolecolumn from users table - 002_teams.sql (both dialects): added Admins group seed, removed
token_budget_daily,token_budget_monthly,allowed_modelscolumns - No new migration files — edited in place per pre-MVP policy
[v0.1.0] — 2026-03-26
Initial release. Pure extension platform — kernel provides auth, RBAC, storage, and the Starlark sandbox. Everything else is a package.
Removed
- AI/Chat system: providers, model catalog, routing policies, personas, channels, messages, completion streaming, tool loop, compaction, memory, knowledge bases, notes, workspaces, projects, folders, files, export/import
- Task scheduler: entire scheduler package, task store, task handlers. Tasks will be rebuilt as a Starlark extension with three trigger primitives (time, webhook, event)
- Session system: channel-based anonymous sessions. Workflow instances will get new storage in v0.2.0
- Health accumulator: provider health windows, tool health tracking. Replaced with kernel-only Prune (ws_tickets, rate_limit_counters, presence)
- 15 Go packages: tools, compaction, extraction, roles, mentions, notelinks, export, memory, knowledge, providers, routing, capabilities, filters, retention, workspace
- 29 handler files, 6 test files, ~44K lines total
Fixed
- CI deploy: k8s resource quantity vars (
BE_MEMORY_REQUEST→MEMORY_REQUEST) aligned with CI workflow outputs —envsubstwas producing empty strings - CI deploy: image var (
BE_IMAGE→IMAGE) — causedInvalidImageNamein pods - CI rollout: deployment name (
switchboard→armature-be) — rollout verification was looking for wrong deployment name - Nginx BASE_PATH: regex cache-header locations intercepted static asset requests before alias could strip the sub-path prefix — moved inside alias block
- Post-login blank page: dead Go template references (
surface-chat,surface-notes,surface-projects) caused html/template to silently produce Content-Length: 0 responses - Login branding: "Chat Armature" → "Armature", updated tagline and feature pills to reflect platform pivot
Changed
- Go module:
armature - VERSION:
0.1.0 - Default DB name:
armature - Fresh migrations: 9 files × 2 dialects (postgres + sqlite), 27 tables
- Store interfaces: 40 → 20 (13 in interfaces.go + 7 in separate iface files)
- Stage modes:
chat_onlyremoved,customadded - Task output modes:
channel|note|webhook→notification|webhook|log - Kernel permissions: 16 → 6 (
extension.use,extension.install,workflow.create,workflow.submit,admin.view,token.unlimited) - Everyone group seed:
["extension.use","workflow.submit"] - Global settings seed: site name "Armature"
- Config: removed 7 dropped fields (SessionExpiryDays, WorkflowStaleHours, ProviderAutoDisableThreshold, ExtractionConcurrency, etc.)
- Health stores rewritten: kernel-only Prune for stale tickets, counters, presence
- Maintenance goroutine replaces scheduler for background cleanup
Retained
- Identity & auth (builtin, mTLS, OIDC)
- Teams, groups, permissions
- Package system (surfaces, extensions, libraries, workflows)
- Starlark sandbox with capability-gated modules
- Extension connections & dependencies
- Workflow definitions, stages, versions
- Notifications & preferences
- Audit log
- Object storage (PVC, S3)
- WebSocket hub & presence
- Multi-replica HA (ws_tickets, rate_limit_counters)
- Frontend shell (preact+htm, SDK, vendor libs)