Feat v0.8.5 extension composability (#72)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-frontend (push) Successful in 6s
CI/CD / test-go-pg (push) Successful in 2m44s
CI/CD / test-sqlite (push) Successful in 2m54s
CI/CD / build-and-deploy (push) Successful in 52s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #72.
This commit is contained in:
2026-04-03 11:33:47 +00:00
committed by xcaliber
parent 3c403dd884
commit 98fd3eb3e6
15 changed files with 850 additions and 531 deletions

View File

@@ -2,6 +2,68 @@
All notable changes to Armature are documented here. All notable changes to Armature are documented here.
## 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 `slots` in their manifest (e.g., `toolbar-actions`,
`note-footer`) with context documentation for extension authors.
- Extensions declare `contributes` entries 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 declares `exports`, not
just `type: "library"` packages. A full package (with surfaces, settings,
UI) can export callable functions for cross-package use.
- The existing `depends` and permission model is unchanged — the called
function runs with the target package's permissions.
**Admin slots endpoint**
- `GET /api/v1/admin/slots` returns 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` — added `slots`, `contributes`, `depends` field docs.
- `EXTENSION-GUIDE.md` — new "Extension Composability" section with slot
naming conventions, contribution patterns, and cross-package call examples.
- `STARLARK-REFERENCE.md` — updated `lib` module to reflect exports-based
calling (not library-type-only).
**Modified files:**
- `server/sandbox/lib_module.go` — type check → exports check
- `server/handlers/extensions.go` — composability field validation
- `server/handlers/packages.go` — orphaned contribution warning
- `server/main.go` — admin slots route registration
- `src/js/sw/sdk/slots.js``renderAll()`, `declare()`, `declarations()`
- `docs/PACKAGE-FORMAT.md` — slots, contributes, depends
- `docs/EXTENSION-GUIDE.md` — composability section
- `docs/STARLARK-REFERENCE.md` — lib module update
- `docs/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 ## v0.8.4 — Documentation Refresh + Surface Sizing Fix
Eight versions of module additions (v0.7.5v0.8.3) shipped without a Eight versions of module additions (v0.7.5v0.8.3) shipped without a

View File

@@ -1,6 +1,6 @@
# Armature — Roadmap # Armature — Roadmap
## Current: v0.8.5 — Extension Composability ## Current: v0.9.x — Workflow Redesign
Self-hosted extensible platform kernel. Auth, identity, packages, Starlark Self-hosted extensible platform kernel. Auth, identity, packages, Starlark
sandbox, storage, realtime, and ops are kernel primitives. Everything else sandbox, storage, realtime, and ops are kernel primitives. Everything else
@@ -9,563 +9,213 @@ is an extension.
**Kernel capabilities:** Auth (builtin/mTLS/OIDC) · Users/teams/groups/RBAC · **Kernel capabilities:** Auth (builtin/mTLS/OIDC) · Users/teams/groups/RBAC ·
Surfaces/extensions/libraries/workflows · Starlark sandbox (capability-gated) · Surfaces/extensions/libraries/workflows · Starlark sandbox (capability-gated) ·
Object storage (PVC/S3) + ext_data tables · WebSocket hub + realtime pub/sub · Object storage (PVC/S3) + ext_data tables · WebSocket hub + realtime pub/sub ·
Audit log · Notifications · Scheduled tasks · Cluster registry + HA Audit log · Notifications · Scheduled tasks · Cluster registry + HA ·
Extension composability (slots/contributes/cross-package calls)
**Completed history:** v0.2.xv0.5.x fully documented in `CHANGELOG.md`.
Highlights: RBAC + settings cascade, event bus + triggers, SDK stabilization,
workflow engine (multi-stage, team roles, signoff gate, public entry, SLA),
package distribution, Notes surface (CM6, folders, tags, backlinks, graph),
realtime primitive, Chat surface (chat-core library + surface + polish),
upgrade test harness, cluster registry + HA.
--- ---
## v0.6.x — Completed (MVP + Hardening) ## Completed — v0.6.x through v0.8.x
All v0.6.x work is shipped and documented in `CHANGELOG.md`. Summary: All completed work is documented in `CHANGELOG.md`.
| Version | Title | Key Deliverables | ### v0.6.x — MVP + Hardening
|---------|-------|-----------------|
| v0.6.0 | Cluster Registry + HA | PG-backed node registry, heartbeat sweep, LISTEN/NOTIFY routing, self-eviction | | Version | Title |
| v0.6.1 | Backup/Restore + Docs | `.swb` archive format, server-side backups, docs surface + 5 guides | |---------|-------|
| v0.6.2 | Docs Polish + OpenAPI | Dark mode fix, topbar nav, `api_schema` manifest field, dynamic spec builder | | v0.6.0 | Cluster Registry + HA |
| v0.6.3 | Dead Code Sweep | Registry install fix, dead Go/JS/HTML deletion, narrowed default bundle | | v0.6.1 | Backup/Restore + Docs |
| v0.6.4 | Admin Health/Metrics | Cluster dashboard merged into Admin tab, block renderer `requires` removed | | v0.6.2 | Docs Polish + Dynamic OpenAPI |
| v0.6.5 | Renderer Pipeline | `sw.renderers.register()` kernel primitive, unified markdown, docs rewrite | | v0.6.3 | Dead Code Sweep + Registry Fix |
| v0.6.6 | Final Hardening | Dependency auto-activation, `ValidateManifest()`, OIDC nonce, ICD/SDK update | | v0.6.4 | Admin Health/Metrics + Cluster Merge |
| v0.6.7 | Native mTLS | `TLS_MODE` config, `MTLSNativeProvider`, node-to-node mTLS, `armature-ca.sh` | | v0.6.5 | Renderer Pipeline + Docs Rewrite |
| v0.6.8 | Cookie Fix + UI Roadmap | Cookie SameSite fix, UI hardening roadmap published | | v0.6.6 | Final Hardening |
| v0.6.9 | Session Lifetime Config | Admin-configurable TTLs, idle timeout, "keep me logged in" | | v0.6.7 | Native mTLS |
| v0.6.10 | Viewport Foundation | Single layout model, CSS zoom, 100dvh, dead shell deprecated | | v0.6.8 | Rebrand + Cookie Fix |
| v0.6.11 | CSS Deduplication | Old primitive system retired, one class per concept | | v0.6.9 | Session Lifetime Config |
| v0.6.12 | Extension CSS Isolation | Prefix enforcement via linter, all 12 in-tree packages migrated | | v0.6.10 | Viewport Foundation |
| v0.6.13 | Responsive & Spacing | Spacing token scale (4px grid), tablet breakpoint | | v0.6.11 | CSS Deduplication |
| v0.6.14 | Visual Polish | Stale fallback colors purged, fonts self-hosted, radius tokens | | v0.6.12 | Extension CSS Isolation |
| v0.6.15 | User Display Audit | Batch user resolve API, `sw.users` SDK module | | v0.6.13 | Responsive & Spacing |
| v0.6.16 | Usability Survey Gate | Four audit scripts, contrast/touch-target fixes | | v0.6.14 | Visual Polish |
| v0.6.17 | Bug Fixes & Welcome | Notes folder fix, team member add fix, welcome auto-disable, zero default bundle | | v0.6.15 | User Display Audit |
| v0.6.18 | CI Bundle Wiring | `BUNDLED_PACKAGES` env var wired into Gitea CI pipeline | | v0.6.16 | Usability Survey Gate |
| v0.6.17 | Bug Fixes & Welcome |
--- | v0.6.18 | CI Bundle Wiring |
## v0.7.x — Test Infrastructure + Quality Gate ### v0.7.x — Test Infrastructure + Quality Gate
The v0.6.x series built the kernel. v0.7.x makes it provably correct. | Version | Title |
|---------|-------|
A full surface audit (docs/AUDIT-surfaces.md) found 7 cross-surface issues | v0.7.0 | Shell Contract + Surface Audit + Rebrand |
and 18 surface-specific issues across Settings, Admin, Team Admin, and Docs. | v0.7.1 | Surface Runner Framework |
Only Docs is properly built. The fix is three phases: (1) establish a shell | v0.7.2 | Package Runners + CI Gate |
contract and bring all four primary surfaces to parity, (2) build a runner | v0.7.3 | Extension Shell Migration |
framework for end-to-end browser tests, (3) automate those runners headlessly | v0.7.4 | Documentation + Surface Work |
in CI. | v0.7.5 | Headless E2E + CI Gate |
| v0.7.6 | Code Hygiene + Test Coverage |
Design docs: | v0.7.7 | API Tokens + Extension Permissions |
- `docs/DESIGN-shell-contract.md` — two-slot topbar, three navigation patterns, surface migrations | v0.7.8 | Bug Fixes & Admin Gaps |
- `docs/DESIGN-surface-runners.md` — runner framework, requires declarations, headless E2E | v0.7.9 | Workflow Independence Audit |
- `docs/AUDIT-surfaces.md` — full audit findings | v0.7.10 | Workflow Handoff + Assignment UI |
| v0.7.11 | Query & HTTP Ergonomics |
### v0.7.0 — Shell Contract + Surface Audit + Rebrand Cleanup | v0.7.12 | Concurrent Execution Primitive |
Design doc: `docs/DESIGN-shell-contract.md` ### v0.8.x — Storage Primitives + Composability (Kernel Complete)
**Shell Infrastructure** | Version | Title |
|---------|-------|
| Step | Status | Description | | v0.8.0 | `files` Module |
|------|--------|-------------| | v0.8.1 | `workspace` Module |
| Shell topbar (two-slot model) | done | Kernel injects topbar into `surface-extension` template. Two named slots: **left** (defaults to manifest title) and **center** (`flex: 1`, for tabs/pickers/search). Home link, notification bell, and user menu always present. | | v0.8.2 | Capability Negotiation |
| Topbar customization API | done | `sw.shell.topbar.setLeft(vnode)` overrides left slot. `sw.shell.topbar.setSlot(vnode)` sets center slot. `sw.shell.topbar.setTitle(str)` shorthand for text-only left. `sw.shell.topbar.hide()` / `.show()` for full-bleed surfaces. | | v0.8.3 | Vector Column Type |
| Kernel tab CSS | done | `.sw-topbar__tabs` and `.sw-topbar__tab` classes for consistent tab styling in the center slot. Surfaces use these for free or style their own slot content. | | v0.8.4 | Documentation Refresh + Surface Sizing Fix |
| Notification read broadcast | done | Backend emits `notification.read` and `notification.all_read` WS events. Bell listens for `.created`, `.read`, `.all_read`. Cross-surface sync without refetch. | | v0.8.5 | Extension Composability |
| User menu reactivity | done | Emit `package.changed` (install/uninstall/enable/disable) and `auth.changed` (role/membership) WS events. UserMenu listens and re-fetches surface list. Most impactful single fix. |
| Shell announcement global dismiss | done | Dismissed state persisted to localStorage keyed by content hash. Dismiss once, dismissed everywhere. |
**Surface Migrations**
| Step | Status | Description |
|------|--------|-------------|
| Settings → Pattern B (flat tabs) | done | Delete custom topbar + sidebar nav. 6 sections become flat tabs in topbar center slot. Content full-width. Fix Teams section: add team admin link, role display, leave action. Remove sessionStorage return URL logic. |
| Admin → Pattern C (category tabs + sidebar) | done | Delete custom `admin-topbar`. `setLeft()` for favicon + "Administration". `setSlot()` for category tabs (People / Workflows / System / Monitoring). Admin sidebar (sub-navigation) unchanged — surface-owned, below the topbar. Bell + user menu come free from shell. Delete bespoke CatIcon renderer if using standard SVGs. |
| Team Admin → Pattern B (flat tabs) | done | Delete custom topbar + sidebar nav. 5 sections (Members / Connections / Workflows / Settings / Activity — Groups removed) become flat tabs. Content full-width. `setTitle()` for team-specific name. Remove sessionStorage return URL. Fix signoff user display (`user_id``sw.users.displayName()`). |
| Team Admin: remove Groups tab | done | 37-line dead-end. Read-only "No groups" with no create/docs/link. Remove until team-scoped group management is properly designed. |
| Docs → Pattern A (default) | done | Delete explicit Topbar import. Shell topbar auto-renders with manifest title. Docs sidebar (document list) is in content area, unaffected. |
**Error Handling + UX Pass**
| Step | Status | Description |
|------|--------|-------------|
| Inline error states | done | Replace `catch { toast }` with inline error + retry on all list endpoints. New `.sw-inline-error` CSS primitive. Systematic pass across Settings, Admin, Team Admin. |
| Empty state guidance | done | Every "No X" message gets one-line explanation + primary action (create button or doc link). Admin Groups, Workflows, Teams; Team Admin Workflows; Settings Notifications. |
**Bug Fixes**
| Step | Status | Description |
|------|--------|-------------|
| evil-chat cleanup | done | ICD security tier: `finally` cleanup block + tighten `409` assertion. |
| Workflow demo error surfacing | done | Replace silent `catch` with inline error + retry. |
| Hello dashboard removal | done | Delete `packages/hello-dashboard/`. |
**Rebrand**
| Step | Status | Description |
|------|--------|-------------|
| Light-mode icon SVG | done | New `favicon-light.svg` — square icon, transparent bg, dark node fills. Rename current `favicon-light.svg` (wordmark) to `wordmark.svg`. |
| Dark-mode wordmark SVG | done | New `wordmark-dark.svg` — light text for dark backgrounds. |
| Light-mode raster assets | done | `favicon-light-32.png`, `favicon-light-256.png`. |
| PWA manifest description | done | "Self-hosted extension platform — build, compose, and run extensions." |
| REBRAND-SPEC.md | | Land into `docs/`. Find/replace patterns, validation checklist, asset inventory. |
| base.html favicon swap | done | Verify theme swap works with new square light icon. |
**Tests**
| Step | Status | Description |
|------|--------|-------------|
| Shell topbar renders for extensions | done | Home, left slot, center slot, bell, user menu present. |
| Topbar API (setLeft, setSlot, hide) | done | Custom content renders. Hide removes topbar. |
| All 4 surfaces use shell topbar | done | No double topbars. Each pattern (A/B/C) renders correctly. |
| User menu reactive to package install | | Install → menu updates without reload. |
| Notification bell cross-surface sync | | Dismiss on Notes → clears on Chat. |
| Inline error on API failure | | Error + retry shown, not empty list. |
### v0.7.1 — Surface Runner Framework
Design doc: `docs/DESIGN-surface-runners.md`
| Step | Status | Description |
|------|--------|-------------|
| Runner framework (`sw.testing`) | done | SDK module: suite/test/assert, lifecycle hooks, structured JSON results. |
| `requires` declarations | done | Runner manifests declare dependencies. Missing packages → clean skip. |
| Cleanup enforcement | done | `s.track(type, id)` auto-deletes in `afterAll`. No leaked state. |
| Warning tier | done | pass / fail / warning. No silent catch swallowing. |
| ICD runner migration | done | Refactor to `sw.testing`. Test logic preserved. |
| SDK runner migration | done | Refactor to `sw.testing`. Domain suites preserved. |
| Runner registry surface | done | `/s/test-runners` — list runners, run-all, results dashboard. |
### v0.7.2 — Package Runners + CI Gate
| Step | Status | Description |
|------|--------|-------------|
| Notes runner | done | `requires: ["notes"]`. 3 suites (crud, folders, tags-search), 12 tests. |
| Chat runner | done | `requires: ["chat", "chat-core"]`. 2 suites (conversations, messaging), 9 tests. |
| Schedules runner | done | `requires: ["schedules"]`. 1 suite (crud), 5 tests. |
| Workflow runner | done | `requires: ["content-approval"]`. 1 suite (lifecycle), 5 tests. |
| Renderer runner | done | `requires: ["mermaid-renderer"]`. 1 suite (contract), 4 tests. |
| Runner result API | done | `POST/GET /api/v1/admin/test-runners/results`. In-memory store, 4 Go tests. |
| CI integration | done | `test-runners` stage in Gitea CI. Playwright driver, `wait-for-healthy.sh`. |
| CI DinD networking fix | done | Resolve container IP via `docker inspect` — DinD port mapping doesn't expose to runner localhost. |
### v0.7.3 — Extension Shell Migration
Chat, Notes, and Schedules still use the old `sw.shell.Topbar` component,
producing a double topbar (shell-injected + surface-owned). Migrate all
three to the v0.7.0 shell contract (`sw.shell.topbar.setTitle/setSlot`),
same pattern as the kernel surface migrations.
| Step | Status | Description |
|------|--------|-------------|
| Chat → shell topbar | done | Delete `<Topbar>`, use `setTitle('Chat')` + `setSlot()` for thread title/people button. |
| Notes → shell topbar | done | Delete `<Topbar>`, use `setTitle('Notes')` + `setSlot()` for action buttons. |
| Schedules → shell topbar | done | Delete `<Topbar>`, use `setTitle('Schedules')` + `setSlot()` for count/new button. |
| Update package runner tests | done | Shell-topbar test suite in each runner — assert no legacy Topbar, uses shell API. |
### v0.7.4 — Documentation + Deferred Surface Work
| Step | Status | Description |
|------|--------|-------------|
| Docs category grouping | done | Backend `Category` field on doc entries. Frontend groups sidebar by category with headings. Four categories: Getting Started, Platform, Extension Development, Operations. |
| Permissions & Groups guide | done | RBAC model, 7 permission slugs, system/custom groups, settings cascade, extension permissions. |
| Workflows user guide | done | Entry modes, stages, team roles, signoff gates, SLA, public forms, branch rules, Starlark hooks. |
| Starlark Reference | done | Sandbox constraints, 10 modules with function signatures, permission gates, example hook script. |
| Frontend JS Guide | done | Preact+htm runtime, 16 SDK modules with API reference, shell topbar patterns, CSS contract. |
| Extension config_section docs | done | Manifest schema, backend discovery, frontend contract, example component. Added to Extension Guide. |
| Docs content refresh | done | All 10 user-facing docs reviewed for v0.7.x accuracy: rebrand volume names, stale CSS vars, TLS_MODE env, self-hosted font notes, Architecture frontend section. |
| Team Admin Workflows split | done | 722-line `workflows.js` split into 3 modules: `workflows.js` (router+CRUD), `workflow-editor.js` (editor+stages), `workflow-monitor.js` (assignments+monitor+signoff). |
| Stale CSS variable fix | done | `--bg-2` references in `sw-shell.css` and `sw-primitives.css` replaced with `--bg-secondary`. |
### v0.7.5 — Headless E2E + CI Gate
| Step | Status | Description |
|------|--------|-------------|
| CI gating redesign | done | Fix path rules: `VERSION`/`scripts/*` deploy-only, `docs/*` no longer skips deploy, test-runners trigger on any code change. |
| E2E smoke test harness | done | `ci/e2e-smoke-test.sh` + `ci/e2e-smoke-driver.js` — Playwright visits every surface, asserts topbar, no JS errors, home link. |
| Screenshot-on-failure | done | Full-page screenshot + console log saved as CI artifacts. |
| CI pipeline integration | done | Re-enable `test-runners` stage, add `e2e-smoke` stage. Failure blocks merge. |
| Design docs landed | done | `docs/DESIGN-storage-primitives.md`, `docs/DESIGN-extension-composability.md`. Roadmap expanded through v1.0. |
### v0.7.6 — Code Hygiene + Test Coverage
Codebase analysis (April 2026) found 9 code smells from the chat gut and
a test coverage gap in the store layer. Fix before v0.8.x kernel expansion.
**Critical Fixes**
| Step | Status | Description |
|------|--------|-------------|
| Remove channels from allowedViews | done | `db_module.go``ext_view_channels` does not exist. `db.view("channels")` crashes. |
| Fix workflow.html dead routes | done | Template calls `/api/v1/channels/{id}/workflow/*` — routes moved to `/api/v1/public/workflows/`. Update template to match. Also fixed `RenderWorkflow` handler to use route param as entry token. |
**Dead Code Removal**
| Step | Status | Description |
|------|--------|-------------|
| Delete SeedTestChannel() | done | `database/testhelper.go` — inserts into nonexistent channels table. |
| Remove RunContext.ChannelID | done | `sandbox/runner.go` — vestigial field, unused by any module. |
| Remove webhook.ChannelID | done | `webhook/webhook.go` — vestigial struct field from chat era. |
| Fix stale comments | done | `storage.go` key format, `prometheus.go` route pattern, `workflow_module.go` param name — reference dead `/channels` paths. |
**Migration Hygiene**
| Step | Status | Description |
|------|--------|-------------|
| Align SQLite migration numbering | done | SQLite 013 placeholder + renumber test_runner_type to 014. Compat rename in migrate.go. |
| Document missing 008 | done | Comment in both 009 files explaining the gap (merged into 007). |
**Test Gap Closure**
| Step | Status | Description |
|------|--------|-------------|
| store/ unit tests | done | Moved to v0.7.9. 17 SQLite store tests: workflow CRUD, stages, instances, lifecycle, tokens, users, groups. |
| workflow/ routing unit tests | done | 82 tests: ResolveNextStage, ResolveStageByName, ParseStageConfig, evaluateCondition (all operators). |
| middleware/ unit tests | done | 10 tests: RequirePermission, RequireAdmin, permission caching, RateLimiter (allow/deny/fail-open). |
| InstallPackage decomposition | done | Moved to v0.7.9. Split into 7 phases: receiveUpload, parseAndValidateArchive, extractPackageAssets, registerPackage, applySchemaAndPermissions, resolveDependencies, checkCapabilities. |
**Bug Fixes**
| Step | Status | Description |
|------|--------|-------------|
| Remove Admin Storage tab | done | Dead weight under System — backend endpoint preserved for future Monitoring use. |
| Fix backup download | done | `sw.auth.token()``sw.auth._getToken()`. Both "Download Backup" and server backup download buttons now work. |
### v0.7.7 — API Tokens + Extension Permissions
Personal access tokens (PATs) for programmatic API access, plus
extension-declared user permissions for backend RBAC enforcement.
Unblocks: E2E test auth, CI automation, extension permission gating,
sidecar auth (v0.10.x).
**API Tokens (PATs)**
| Step | Status | Description |
|------|--------|-------------|
| `api_tokens` table + migration | done | `id`, `user_id`, `name`, `token_hash`, `permissions` (JSON array), `expires_at`, `last_used_at`, `created_at`. PG + SQLite. |
| Token store interface | done | `Create`, `GetByHash`, `ListForUser`, `Revoke`, `CleanExpired`, `UpdateLastUsed`. |
| Token generation endpoint | done | `POST /api/v1/auth/tokens` — name, permissions (subset of user's), expires_at. Returns token once (plain text), stored as SHA-256 hash. |
| Token management endpoints | done | `GET /api/v1/auth/tokens` (list mine), `DELETE /api/v1/auth/tokens/:id` (revoke). |
| Admin token endpoint | done | `POST /api/v1/admin/tokens` — creates token for any user. Explicit admin action, audit logged. |
| Auth middleware update | done | Accept `Bearer arm_pat_...` tokens alongside JWTs. Resolve user + scoped permissions from token. PAT requests skip refresh token flow. |
| Permission scoping | done | Token permissions ⊆ creating user's resolved permissions at creation time. Validated on create. If user loses a permission later, token retains it until revoked (git model). |
| Settings UI | done | Settings → API Tokens tab. Create, list, revoke. Show permissions, expiry, last used. Token value shown once on creation. |
| Admin UI | done | Admin → People → user detail → "Create API Token" action. |
| E2E test migration | done | Replace `ci/e2e-smoke-test.sh` login flow with PAT-based auth. Seed token via `ARMATURE_BOOTSTRAP_PAT=true`. |
**Extension-Declared User Permissions**
| Step | Status | Description |
|------|--------|-------------|
| `user_permissions` manifest field | done | Extensions declare permissions users need: `"user_permissions": ["image-gen.use", "image-gen.admin"]`. |
| Dynamic permission registry | done | On install: merge into valid permission set. On uninstall: remove. `AllPermissions` becomes `KernelPermissions + ExtensionPermissions`. |
| Group UI update | done | Admin → Groups → permission checkboxes include extension-registered permissions, grouped by source package. |
| `permissions` Starlark module | done | `permissions.check(user_id, "image-gen.use")` → resolves user's groups, returns bool. New permission: none required (read-only check against kernel data). |
| `gate_permission` manifest field | done | Optional. If set, `ext_api.go` checks this permission before calling `on_request`. Extension doesn't execute for unauthorized users. |
| `req["permissions"]` in request dict | done | `ext_api.go` resolves user permissions and includes them in the request dict. Extensions can check inline without the module. |
### v0.7.8 — Bug Fixes & Admin Gaps
| Step | Status | Description |
|------|--------|-------------|
| Workflow landing page Start | done | Added `StartBySlug` handler + `/api/v1/workflow-entry/:scope/:slug` route. Landing page Start button now resolves scope/slug → workflow ID and creates instance. |
| Workflow delete guard | done | Admin `DELETE /api/v1/workflows/:id` rejects team-scoped workflows (403). Team workflows must use team endpoint. Prevents accidental global template deletion. |
| Package button cleanup | done | Delete button hidden for `bundled` packages via `IMMUTABLE_SOURCES` guard, matching Export/Update. |
| Package export fetch | done | Export uses `fetch()` with auth token instead of `window.open()`. Handles errors, shows toast on missing assets. |
| Settings CSS fix | done | Bottom padding increased to `--sp-12` on `.admin-settings-form`. Save button no longer cut off. |
| Admin stage builder + links | done | Shared `StageForm` component between admin and team-admin. Public entry URL with copy button in admin workflow editor. |
### v0.7.9 — Workflow Independence Audit
Workflows must work end-to-end without chat or any 3rd-party package
dependency. Chat, notifications, and other packages should *enhance*
workflows but never be required for core operation.
**RenderWorkflow Fix (critical)**
| Step | Status | Description |
|------|--------|-------------|
| Fix RenderWorkflow() handler | done | Was a stub (always `stageMode = "custom"`, no data loaded). Now loads instance by token/ID, resolves current stage, populates all template data. |
| Rename WorkflowPageData fields | done | `ChannelID``EntryToken`, `ChannelTitle``WorkflowTitle`, `ChannelDescription``WorkflowDescription`. Vestigial chat-era names removed. |
| Align stage mode values | done | Template now uses Go constants (`form`, `review`, `delegated`, `automated`) instead of legacy names (`form_only`, `form_chat`). |
| Remove dead chat UI | done | Chat CSS, split layout, `sendMessage()` function, and fallback chat branch all removed from `workflow.html`. Replaced with clean form/review/unsupported modes. |
| Fix landing page mode mapping | done | `workflow-landing.html` conditionals updated to match Go stage mode constants. |
| Update OpenAPI spec | done | `stage_mode` enum changed from `[custom, form_only, form_chat, review]` to `[form, review, delegated, automated]`. |
**Verification & Testing**
| Step | Status | Description |
|------|--------|-------------|
| Admin/team-admin UI audit | done | All workflow CRUD, monitoring, stage builder verified clean — zero chat/chat-core references. |
| E2E workflow test | done | `ci/e2e-workflow-nochat.sh` — creates workflow via API, adds form+review stages, starts instance via public entry, verifies landing + execution pages. |
| Store unit tests (SQLite) | done | 17 tests: workflow CRUD, stages, instances, lifecycle (advance/complete/cancel/stale), team scope, API tokens, users, groups. Carried from v0.7.6. |
| InstallPackage decomposition | done | Split 400-line handler into 7 private methods: `receiveUpload`, `parseAndValidateArchive`, `extractPackageAssets`, `registerPackage`, `applySchemaAndPermissions`, `resolveDependencies`, `checkCapabilities`. Carried from v0.7.6. |
**Bug Fixes (found during verification)**
| Step | Status | Description |
|------|--------|-------------|
| Entry token resolution | done | `RenderWorkflow()` passed route param (instance ID) as entry token. JS then sent instance ID to the advance API which expects entry token. Fixed: resolve actual token from `inst.EntryToken` + check `?token=` query param. |
| Fieldset submit guard | done | `submitForm()` checked `FORM_TPL.fields` but not `.fieldsets` — progressive forms silently no-op'd on submit. Fixed: accept either. |
| Stage advance status check | done | Template JS checked `result.status === 'advanced'` but API returns instance status (`active`/`completed`). Fixed: on 200 OK with `active` status, reload to render next stage. |
**Discovered issues (deferred)**
The audit uncovered deeper workflow UX gaps that are out of scope for v0.7.9:
1. **Public→authenticated stage handoff** — After a public visitor completes their stages, the workflow advances to an authenticated stage (e.g. "classify"). The visitor sees the next stage's form + an auth error instead of a "thank you, we'll take it from here" message. The page should detect audience mismatch and show a completion/waiting screen.
2. **Team member pickup UI** — No surface exists for team members to see workflow instances assigned to their team and claim/work on them. Currently only visible in team-admin monitoring.
3. **Assignment flow** — No mechanism for team admins to assign a specific instance to a specific team member. The `workflow_assignments` table exists but has no admin UI for manual assignment.
These will be addressed in a future version (see v0.7.10 below).
### v0.7.10 — Workflow Handoff + Assignment UI
Addresses the three UX gaps found during the v0.7.9 independence audit.
The workflow engine and data model already support these flows — the
missing pieces are page-level audience detection and team-facing UI.
| Step | Status | Description |
|------|--------|-------------|
| Public stage completion screen | done | `RenderWorkflow()` detects audience mismatch (team/system stage + unauthenticated visitor) and renders "Submitted Successfully" screen with reference ID. |
| Team workflow inbox | done | Enhanced `AssignmentsTab` in team-admin: claim/unclaim/work actions, time-ago display, WS live updates. Fixed vestigial `channel_id` references. |
| Manual assignment UI | done | "Assign" button on unassigned rows with team member dropdown. New `POST /api/v1/assignments/:id/assign` endpoint (requires `workflow.create` permission). |
| Assignment notifications | done | Engine calls `notifyAssignment()` on assignment creation — notifies specific user or all team members via notification system. |
| Team workflow instances endpoint | done | `GET /api/v1/teams/:teamId/workflow-instances` + cancel endpoint. `ListInstancesByTeam` store method (SQLite + PG). SDK methods added. |
| API response enrichment | done | `ListByTeam` and `ListMine` handlers enrich assignments with `workflow_name`, `stage_name`, `sla_breached` by joining instance → workflow → version snapshot. Cached per-request. Same for `ListTeamInstances` with `instanceView` struct. |
| Team middleware fix | done | `RequireTeamAdmin` / `RequireTeamMember` system-admin bypass was dead code (`c.Get("role")` never set). Fixed to check `PermSurfaceAdminAccess` via `resolveAndCachePerms`. |
| SDK gap closure | done | Added `workflowAssignments` domain (claim/unclaim/complete/cancel/assign/mine), `teams.assignments`, `teams.workflowInstances`, `teams.cancelWorkflowInstance`. Fixed dead `workflows.cancel` route. |
| E2E multi-stage test | done | `ci/e2e-workflow-handoff.sh`: public form → team review → claim → complete. Verifies audience mismatch screen, assignment creation, full lifecycle. |
### v0.7.11 — Query & HTTP Ergonomics (was v0.7.10)
Four additions to existing sandbox modules. No new files beyond tests,
no schema changes. Motivated by real-world extension porting feedback:
complex SQL decomposition and synchronous HTTP fan-out are the two
remaining friction points for extensions migrating from native Go/Python.
| Step | Status | Description |
|------|--------|-------------|
| `db.count()` | done | `db.count(table, filters={})` → int. `SELECT count(*) FROM ext_{pkg}_{table} WHERE ...`. Uses existing `starlarkFiltersToSQL` builder. Permission: `db.read`. |
| `db.aggregate()` | done | `db.aggregate(table, column, op, filters={})` → single value. `op` ∈ {`count`, `sum`, `avg`, `min`, `max`}. Column name validated same as filter keys. Returns int/float/None. Permission: `db.read`. |
| `db.query_batch()` | done | `db.query_batch(queries)` → list of result sets. Each query spec is a dict with `table` (required), `filters`, `order`, `limit`, `before`, `after`, `search_like` (all optional, same as `db.query`). Loops extracted `buildSelectQuery` helper internally. Permission: `db.read`. |
| `http.batch()` | done | `http.batch(requests)` → list of response dicts (ordered). Each request dict: `method`, `url`, `body`, `headers`. Concurrent dispatch via goroutines capped at 10, `sync.WaitGroup` collection. Reuses existing `executeHTTPRequest` plumbing (SSRF checks, domain allowlist, body cap, timeout). Individual failures return error response dicts, don't abort batch. Permission: `api.http`. |
| Tests | done | 21 new tests (14 db, 7 http). `db.count`/`db.aggregate` test empty table, filtered, type coercion, invalid op/column. `db.query_batch` test mixed specs, empty list, cap, missing table. `http.batch` test blocked domains, partial failure, mixed results, input validation. |
### v0.7.12 — Concurrent Execution Primitive (was v0.7.11)
New `batch` sandbox module. Enables extensions to parallelize arbitrary
Starlark callables — including library function calls via `lib.require()`.
The kernel manages concurrency (multiple `starlark.Thread` instances in
goroutines); extensions stay single-threaded and synchronous from the
author's perspective.
Key insight: library exports loaded via `lib.require()` are frozen structs
(immutable, safe to share across threads). Each concurrent branch gets
fresh module instances (`db`, `http`, etc.) — same pattern as
`buildRestrictedModules` in `triggers/schedule.go`.
| Step | Status | Description |
|------|--------|-------------|
| Design doc | done | `docs/DESIGN-batch-exec.md`. Thread isolation model, module construction, error semantics (partial results on failure), permission model, concurrency cap. |
| `batch.exec()` | done | `batch.exec(callables)``(results, errors)` tuple. List of Starlark callables (lambdas, function refs). Each runs in its own `starlark.Thread` with fresh modules. Concurrent goroutines capped at 8. Ordered results. Errors are per-callable (None on success, string on failure). Permission: `batch.exec` (new). New file: `sandbox/batch_module.go`. |
| Runner wiring | done | `buildModulesWithLibCtx` creates `batch` module when `batch.exec` permission granted. Module receives runner reference for per-branch module construction. |
| Tests | done | 12 unit tests: parallel execution ordering, partial failure, timeout per-branch, parent cancellation, cap enforcement, empty list, non-callable, permission gating, frozen struct sharing, nested batch.exec, default timeout, timeout clamping. All pass with `-race`. |
--- ---
## Planned ## Planned
### v0.8.x — Storage Primitives ### v0.9.x — Workflow Redesign
Design doc: `docs/DESIGN-storage-primitives.md` Design doc: `docs/DESIGN-workflow-redesign.md`
The last major kernel expansion. Completes the primitive set that the The workflow system (~7,600 lines, 25+ files) was built incrementally and
entire extension ecosystem builds on. After v0.8.x, the kernel is has accumulated redundant concepts, buried primitives, and a read-only
feature-complete for 1.0 — all new capabilities are extensions. Starlark module. This series cleans up the debt and promotes reusable
primitives before building reference extensions on top.
**v0.8.0 — `files` Module** **v0.9.0 — Starlark Converter Consolidation + Snapshot Cleanup**
Bridge `ObjectStore` (PVC/S3) into the Starlark sandbox. Extension-scoped Three files contain near-identical Go↔Starlark converters; three copies
key namespacing (`ext/{packageID}/`). Permissions: `files.read`, of the snapshot parser exist. Consolidate into `sandbox/convert.go` and
`files.write`. Metadata companions stored as sibling objects. one exported snapshot function. Standardize on wrapped snapshot format.
New: `sandbox/files_module.go`. Modified: runner wiring, permission **v0.9.1 — Team User Roles**
constants. `ObjectStore` interface gained `List()` method. 16 new tests.
**v0.8.1 — `workspace` Module** Promote the buried role system to a kernel primitive. New
`team_user_roles` table (many-to-many). Manifest `requires_roles` field.
Team admin UI for role assignment. Kernel middleware `RequireRole()`.
Starlark SDK: `teams.get_member_roles()`, `teams.has_role()`.
Managed disk directories for tools that need a real filesystem (git, **v0.9.2 — Package Adoption + Roles**
compilers, media tools). Extension-scoped path namespacing
(`{WORKSPACE_ROOT}/{packageID}/{name}/`). Five builtins: `create`,
`path`, `list`, `delete`, `usage`. Permission: `workspace.manage`.
Quota enforcement via `WORKSPACE_QUOTA_MB`. Path traversal and symlink
escape protection.
New: `sandbox/workspace_module.go`. Modified: runner wiring, permission `scope: adoptable` manifest field. When a team adopts an adoptable
constants, config (`WORKSPACE_ROOT`, `WORKSPACE_QUOTA_MB`), main.go package, the package's `requires_roles` auto-populate into the team's
startup. 16 new tests. role slots. Replaces `AdoptTeamWorkflow` clone mechanism.
**v0.8.2 — Capability Negotiation** **v0.9.3 — Typed Forms → SDK Primitive**
Extensions declare environment requirements in manifest (`capabilities.required`, Extract `TypedFormTemplate`, `FormField`, `FormFieldset`, etc. from
`capabilities.optional`). Kernel validates at install time — required caps `models/workflow.go` into a `forms` package. FE SDK: `sw.forms.render()`
reject with 422, optional caps log a warning. Runtime query via and `sw.forms.validate()`. Starlark: `forms.validate()`. Any package
`settings.has_capability()`. Admin endpoint: `GET /admin/capabilities`. can declare forms, not just workflow stages.
Detected capabilities: `pgvector`, `workspace`, `object_storage`, `s3`, **v0.9.4 — Deprecate `stage_type`, Collapse `stage_mode`**
`postgres`.
New: `handlers/capabilities.go`, `handlers/capabilities_test.go`, `stage_type` (simple/dynamic/automated) is redundant with `starlark_hook`
`sandbox/settings_module_test.go`. Modified: install handler, bundled presence. Remove from new manifests, keep parsing for backward compat.
handler, settings module, runner, main.go. 18 new tests. Collapse `stage_mode` from 4 to 3 values: form / delegated / automated.
`review` was just `delegated` with signoff — signoff is independent of
rendering mode.
**v0.8.3 — Vector Column Type** **v0.9.5 — Full Read/Write Workflow Starlark Module**
`"vector(N)"` in `db_tables` manifest. Maps to native `vector(N)` on Add `workflow.start()`, `workflow.advance()`, `workflow.cancel()`,
PG+pgvector, `JSONB` on PG without, `TEXT` on SQLite. New `db.query_similar()` `workflow.submit_signoff()` to the Starlark module. Extract engine
with dual-path dispatch (native operator vs Go-side brute force). Auto-created interface to break circular import. Unlocks fully automated workflow
HNSW index on pgvector. `starlarkToGoValue` extended with list→JSON. orchestration from Starlark hooks and extensions.
New: `docs/DESIGN-vector-column.md`. Modified: `ext_db_schema.go`, **v0.9.6 — Conditional Routing → SDK Primitive**
`db_module.go`, `runner.go`, `extensions.go`, `main.go`. 13 new tests.
**v0.8.4 — Documentation Refresh + Surface Sizing Fix** Expose `routing.evaluate(rules, data)` as a Starlark SDK function.
Branch rules become a reusable decision engine for any extension.
Docs pass covering v0.7.5v0.8.3 additions. STARLARK-REFERENCE.md gains **v0.9.7 — Multi-Surface Manifest + Route Resolution**
`workspace` module, `permissions` module, `settings.has_capability()`.
EXTENSION-GUIDE.md gains `capabilities` manifest, `vector(N)` column type,
`user_permissions`, `gate_permission`, updated permissions list. Surface
sizing fix: `.surface-inner` converted to flex column layout so the shell
topbar and surface container share vertical space correctly — eliminates
44px scroll cutoff on all surfaces.
**v0.8.5 — Extension Composability** Packages can declare multiple surface routes with independent access
controls. Kernel resolves routes per-page. Enables mixed public/auth
pages in a single package.
Design doc: `docs/DESIGN-extension-composability.md` **v0.9.8 — Surface Access via Roles**
Manifest declarations for `slots` (host surfaces declare injection points) Wire team roles (v0.9.1) into surface access declarations:
and `contributes` (extensions declare UI contributions to other packages' `access: role:approver`. Kernel middleware checks role membership.
slots). `lib.require()` relaxed from library-only to any package with Completes the workflow→package access story.
`exports` — enables full packages (with surfaces, settings, UI) to also
expose callable backend functions. `sw.slots.renderAll()` SDK helper.
Admin slot aggregation endpoint.
The composability primitive that enables: LLM tool registration, UI
contribution (toolbar buttons, image actions), and cross-extension
function calls. No new tables, migrations, or permissions.
Modified: `sandbox/lib_module.go` (~1 line), `handlers/extensions.go`,
`src/js/sw/sdk/slots.js`. New: `handlers/admin_slots.go`.
--- ---
### v0.9.x — Reference Extensions ### v0.10.x — Reference Extensions
The kernel is complete. This series proves the platform by shipping The kernel is complete. This series proves the platform by shipping
first-party extensions that exercise every primitive. These are first-party extensions that exercise every primitive. These are
**extensions, not kernel code** — they ship as `.pkg` files and can be **extensions, not kernel code** — they ship as `.pkg` files and can be
uninstalled. uninstalled.
**v0.9.0 — `vector-store` Library** **v0.10.0 — `vector-store` Library**
Library extension: document ingestion, chunk storage, embedding persistence, Document ingestion, chunk storage, embedding persistence, semantic
semantic similarity search. Three-tier vector strategy (pgvector → JSONB similarity search. Consumes: `db.write`, `files.read`.
fallback → TEXT fallback). Consumes: `db.write`, `files.read`.
**v0.9.1 — `llm-bridge` Library** **v0.10.1 — `llm-bridge` Library**
Library extension: model abstraction, tool-use routing, provider BYOK via Model abstraction, tool-use routing, provider BYOK via connections.
connections. Exposes `complete()`, `embed()`, `classify()`, `register_tool()` Exposes `complete()`, `embed()`, `classify()`, `register_tool()`.
to consumer extensions. Tool extensions (image-gen, code-exec, web-search)
register at load time — LLM discovers available tools automatically.
Consumes: `connections.read`, `api.http`, `db.write`. Consumes: `connections.read`, `api.http`, `db.write`.
**v0.9.2 — `file-share` Extension** **v0.10.2 — `file-share` Extension**
File upload via `api_routes`, storage via `files` module, download links, File upload, storage via `files` module, download links, team/group
optional team/group ACLs via resource grants. First extension to exercise ACLs via resource grants.
the full `files` primitive end-to-end.
**v0.9.3 — `code-workspace` Extension** **v0.10.3 — `code-workspace` Extension**
Managed code repositories via `workspace` module. Git clone/pull via Managed code repos via `workspace` module. Git operations, file browser
`api.http` to Gitea/GitHub API. File browser surface. Structural indexing surface, structural indexing. Consumes: `workspace.manage`, `files.write`.
(AST-aware) for code navigation. Consumes: `workspace.manage`, `files.write`,
`api.http`.
**v0.9.4 — `image-gen` + `image-edit` Extensions** **v0.10.4 — `image-gen` + `image-edit` Extensions**
Image generation via external APIs (DALL-E, Stable Diffusion). Registers Image generation/editing via external APIs. Composability demo:
as LLM tool with `llm-bridge`. Contributes regen/edit buttons to contributes to `chat:image-actions` slot.
`chat:image-actions` slot. Image-edit extends with inpaint, outpaint,
upscale, and style transfer — each contributing action buttons to the
same slot independently. First full demonstration of the composability
pattern: three extensions contributing UI to a fourth's surface.
**v0.9.5 — Chat System** **v0.10.5 — Chat System**
Generic 1-to-N human messaging. `chat-core` library (conversation/message Generic 1-to-N messaging with slots. `chat-core` library + `chat`
CRUD) + `chat` surface. Declares `chat:message-actions`, `chat:image-actions`, surface. Declares `chat:message-actions`, `chat:image-actions`,
`chat:composer-tools` slots for extension contribution. LLM participation `chat:composer-tools` slots. Consumes: `db.write`, `realtime.publish`,
as a follow-on consuming `llm-bridge`. Consumes: `db.write`, `files.write`.
`realtime.publish`, `files.write` (attachments).
--- ---
### v0.10.x — Sidecar Tier + Polish ### v0.11.x — Sidecar Tier + Polish
**v0.10.0 — Sidecar Tier** **v0.11.0 — Sidecar Tier**
Autonomous out-of-process extensions for workloads that can't run in Out-of-process extensions for workloads that can't run in Starlark (ML
Starlark: ML inference, media transcoding, language servers, local git inference, media transcoding, language servers, local git). Sidecars are
operations. The kernel does NOT manage container lifecycle — sidecars independent processes that connect inward to the kernel, following the
are independent processes (Docker, systemd, bare binary) that connect cluster registry pattern. Authentication via mTLS or registration tokens.
inward to the kernel, following the cluster registry pattern:
- Sidecar boots with cluster config (instance URLs, credentials). **v0.11.1 — Native Dialog Audit**
- Connects and self-registers, declaring capabilities ("sklearn",
"gpu", "ffmpeg", "sentence-transformers").
- Goes inert until an admin authorizes (same flow as extension permissions).
- Kernel dispatches work; sidecar returns results.
- Heartbeat keeps registration alive; missed heartbeat = eviction.
Communication channel TBD (WebSocket over existing event bus, gRPC, or
HTTP callback model — design doc required). Authentication via mTLS
certs or registration tokens.
This model eliminates k8s RBAC dependency for local deployments. The
kernel stays thin — it accepts registrations and dispatches work. The
sidecar brings its own compute and manages its own lifecycle.
**v0.10.1 — Native Dialog Audit**
Replace `prompt()`/`confirm()`/`alert()` with `sw.dialog` SDK primitives. Replace `prompt()`/`confirm()`/`alert()` with `sw.dialog` SDK primitives.
Accessible, themed, promise-based. Small kernel SDK change that improves
every surface.
**v0.10.2 — Stability + Migration Tooling** **v0.11.2 — Stability + Migration Tooling**
Proper versioned migrations (post-MVP discipline). `armature migrate` Versioned migrations, `armature migrate` CLI, backup/restore validation,
CLI command. Backup/restore validation against reference dataset. pre-1.0 schema freeze.
Pre-1.0 schema freeze.
--- ---
### v1.0.0 — Stable Release ### v1.0.0 — Stable Release
The kernel API surface is frozen. Extensions are the product. Kernel API surface frozen. Extensions are the product.
Gate criteria: Gate criteria:
- All kernel Starlark modules documented with examples - All kernel Starlark modules documented with examples
- All `api_routes` covered by OpenAPI spec - All `api_routes` covered by OpenAPI spec
- Upgrade path tested from v0.8.0 → v1.0.0 - Upgrade path tested from v0.8.0 → v1.0.0
- At least 3 reference extensions shipped and tested (vector-store, llm-bridge, chat) - At least 3 reference extensions shipped (vector-store, llm-bridge, chat)
- At least 1 cross-extension composability demo (image-gen → chat:image-actions) - At least 1 cross-extension composability demo (image-gen → chat:image-actions)
- Headless E2E green on PG + SQLite - Headless E2E green on PG + SQLite
- `armature-ca.sh` + mTLS deployment guide - `armature-ca.sh` + mTLS deployment guide
@@ -611,29 +261,14 @@ These are candidates, not commitments. Each requires a design doc.
| Single Docker image | Go binary + assets + migrations. | | Single Docker image | Go binary + assets + migrations. |
| Admin → RBAC group | Grant check replaces role check. | | Admin → RBAC group | Grant check replaces role check. |
| Settings cascade | Scope auth + `user_overridable`. Two orthogonal axes. | | Settings cascade | Scope auth + `user_overridable`. Two orthogonal axes. |
| No new migrations pre-MVP | Proper versioned migrations post-MVP. |
| Chat as extension, not kernel | Zero kernel awareness. Proves extensibility thesis. | | Chat as extension, not kernel | Zero kernel awareness. Proves extensibility thesis. |
| PG as consensus layer | UNLOGGED node_registry + LISTEN/NOTIFY. No etcd/Consul/Redis. | | PG as consensus layer | UNLOGGED node_registry + LISTEN/NOTIFY. No etcd/Consul/Redis. |
| Two trigger tiers | Extension-declared (full sandbox) vs user ad-hoc (restricted). | | Two trigger tiers | Extension-declared (full sandbox) vs user ad-hoc (restricted). |
| Builtin package rationale | Must enhance kernel surfaces or demonstrate platform capabilities. | | Two-slot topbar model | Left slot (title/branding) + center slot (`flex: 1`, tabs/pickers). |
| Two-slot topbar model | Left slot (title/branding) + center slot (`flex: 1`, tabs/pickers). Two named slots cover every navigation pattern: simple title (Pattern A), flat tabs full-width (Pattern B), category tabs + surface-owned sidebar (Pattern C). Surfaces without sub-items get full content width; surfaces with hierarchical navigation add their own sidebar below the topbar. Shell provides the stage; surface decides the theater. | | `db` module is the structured store | No separate KV primitive. Extensions declare tables. |
| All four primary surfaces migrate to shell topbar | Admin was "keep custom topbar" initially. The two-slot model makes it unnecessary — category tabs fit in the center slot, sidebar is surface-owned below. One topbar implementation replaces four. Bell + user menu + reactivity come free on every surface. | | `files` module rides ObjectStore | No new kernel tables. Metadata as companion objects. |
| Settings / Team Admin → flat tabs (Pattern B) | Both had thin sidebars (~140px) that consumed width without justification. 56 sections fit cleanly in topbar tabs. Full-width content is a better use of space for these surfaces. | | `workspace` for real filesystem | Flat blob store can't serve git/compilers/ffmpeg. Managed disk paths. |
| Team Admin Groups removed | 37-line read-only dead-end. Admin Groups has full CRUD. Restore when properly designed. |
| Docs is the reference surface | Only surface with shell Topbar, bell, user menu, inline errors. Others converge. |
| Surface runners over expanding ICD | Different test tier, different failure class. |
| Headless E2E via Playwright | Runners produce structured JSON; Playwright navigates and reads output. |
| `db` module is the structured store | No separate KV primitive. Extensions declare tables — infrastructure exists. |
| `files` module rides ObjectStore | No new kernel tables. Metadata as companion objects. Works on PVC and S3 identically. |
| `workspace` for real filesystem | Flat blob store can't serve git/compilers/ffmpeg. Managed disk paths are the explicit escape hatch. |
| Capability negotiation at install | Fail loud with actionable message, not silently at runtime. | | Capability negotiation at install | Fail loud with actionable message, not silently at runtime. |
| Vector column with three-tier fallback | Works everywhere, works fast with pgvector. Documentation > hard gates. | | Vector column with three-tier fallback | Works everywhere, works fast with pgvector. |
| Docs refresh before composability | 8 versions of module additions (v0.7.5v0.8.3) without a docs pass. Ship accurate docs before adding more API surface. | | Sidecar deferred to v0.11.x | HTTP module covers external APIs. Sidecars connect inward (no k8s RBAC needed). |
| Sidecar deferred to v0.10.x | HTTP module covers external APIs. Local compute is a v0.10.x concern. Sidecars connect inward (like cluster nodes), not spawned outward (no k8s RBAC needed). | | Workflow redesign before reference extensions | Clean up debt and promote primitives before building on top. |
| No second scripting runtime | Starlark for glue, HTTP for external compute, sidecars for local compute. Three tiers with clear boundaries. Lua/WASM middle tier rejected — Turing-complete enough to be dangerous, not isolated enough to be trustworthy. |
| Reference extensions prove the platform | First-party `.pkg` files, not kernel code. Uninstallable. Dogfooding. |
| Composability via slots + exports, not kernel | `sw.slots` + `sw.actions` + `lib.require()` are the composition primitives. Manifest declarations add admin visibility. No inter-extension message bus — too complex, too fragile. |
| Gut cleanup before kernel expansion | Codebase analysis found 9 smells from the chat gut. Fix before adding new primitives — dead views and broken templates compound when new code builds on them. |
| PATs over OAuth client credentials | PATs are simpler (one table, one middleware check), cover all use cases (E2E tests, CI, sidecars, user automation), and follow the git hosting convention users already understand. OAuth adds client registration, token exchange, scopes — complexity without benefit for a self-hosted platform. |
| Extension-declared user permissions | Extensions register custom permissions in the kernel's group system. Admin assigns them to groups. Backend enforces via `permissions.check()` or `gate_permission`. Frontend hides via `sw.can()`. The kernel owns the permission registry; extensions contribute to it. |
| Admin token as explicit action | Following Venice.ai pattern — admin token creation is a separate endpoint, audit logged, requires `surface.admin.access`. No ambiguity about the privilege level. |

View File

@@ -1 +1 @@
0.8.4 0.8.5

View File

@@ -1,7 +1,7 @@
# DESIGN — Extension Composability # DESIGN — Extension Composability
**Version:** v0.8.4 **Version:** v0.8.5
**Status:** Draft **Status:** Implemented
**Author:** Jeff / Claude session 2026-04-02 **Author:** Jeff / Claude session 2026-04-02
--- ---

View File

@@ -0,0 +1,126 @@
# DESIGN — Workflow System Redesign (0.9.x)
**Version:** v0.9.0
**Status:** Draft
**Author:** Jeff / Claude session 2026-04-03
---
## Problem
The workflow system spans ~7,600 lines across 25+ files and was built
incrementally from v0.3.x through v0.7.10. It works, but several
concepts are redundant, primitives are buried, and the Starlark module
is read-only. Before shipping reference extensions (v0.10.x) that
build on workflows, the system needs cleanup and promotion of reusable
primitives.
## Audit Summary
Full audit in `/config/Downloads/AUDIT-workflow-0.9.md`. Classification:
### KEEP — Core primitives that survive
| Component | File(s) | Lines | Rationale |
|-----------|---------|-------|-----------|
| Engine core | `workflow/engine.go` | ~400 | Stateless state machine — Start, Advance, Cancel, version-pinned execution |
| Conditional routing | `workflow/routing.go` | ~500 | Pure functions, 8 operators, well-tested (497 lines of tests) |
| Automated processing | `workflow/automated.go` | ~300 | Starlark hook execution, cycle guard (max 10) |
| Scanner | `workflow/scanner.go` | ~200 | Background SLA + staleness checks, 5-minute interval |
| Signoff system | engine.go + handlers | ~400 | Multi-party approve/reject with quorum, role-gated |
| Assignment queue | handlers | ~300 | Claim/unclaim/complete/cancel lifecycle |
| Version snapshots | store + models | ~200 | Immutable snapshots, version-pinned instances |
| Typed form system | `models/workflow.go` | ~300 | 8 field types, fieldsets, conditional visibility |
| Store interface | `store/workflow_iface.go` | 61 methods | Complete lifecycle coverage |
### OBE — Superseded by new design
| Concept | Superseded by |
|---------|---------------|
| Per-stage `audience` field | Multi-page packages with mixed access declarations |
| `entry_mode` (public_link/team_only) | Package-level `scope: adoptable` + per-page access |
| `stage_mode` proliferation (4 values) | Collapse to 3: form / delegated / automated |
| `stage_type` (simple/dynamic/automated) | Redundant with `starlark_hook` presence check |
| `AdoptTeamWorkflow` clone | Package adoption model |
| `ExportWorkflowPackage` endpoint | Standard package export |
### PROMOTE — Buried features to expose as kernel primitives
1. **Roles → kernel primitive**: `team_user_roles` table, manifest `requires_roles`,
team admin UI, kernel middleware, Starlark SDK
2. **Typed forms → SDK primitive**: Move from workflow models to `forms` package,
FE SDK `sw.forms.render()` / `sw.forms.validate()`
3. **Conditional routing → SDK primitive**: Starlark `routing.evaluate(rules, data)`
4. **Workflow Starlark module → full read/write**: `workflow.start()`, `.advance()`,
`.cancel()`, `.submit_signoff()`
---
## Technical Debt
### Starlark Converter Duplication
Three files contain nearly identical Go↔Starlark conversion functions:
| File | Functions |
|------|-----------|
| `workflow/automated.go` | `goToStarlark`, `starlarkDictToMap`, `starlarkToGo` |
| `handlers/workflow_hooks.go` | `starlarkDictToMap`, `starlarkToGo`, `jsonToStarlark` |
| `sandbox/workflow_module.go` | `goValToStarlark` |
**Fix:** Consolidate into `sandbox/convert.go`.
### Snapshot Format Inconsistency
Two formats exist (wrapped `{stages, workflow}` vs legacy flat array).
Three copies of the parser across engine, instance handlers, and
assignment handlers.
**Fix:** Consolidate into one exported function. Standardize on wrapped format.
---
## Sequencing
| Version | Feature | Dependencies |
|---------|---------|-------------|
| **v0.9.0** | Starlark converter consolidation + snapshot format cleanup | None |
| **v0.9.1** | `team_user_roles` table + management API + admin UI | None |
| **v0.9.2** | `scope: adoptable` manifest field + adoption flow with role auto-populate | v0.9.1 |
| **v0.9.3** | Promote typed forms to SDK primitive (`sw.forms`) | None |
| **v0.9.4** | Deprecate `stage_type`, collapse `stage_mode` to 3 values | None |
| **v0.9.5** | Full read/write workflow Starlark module | v0.9.0 |
| **v0.9.6** | Conditional routing as SDK primitive | v0.9.5 |
| **v0.9.7** | Multi-surface manifest + kernel route resolution | None |
| **v0.9.8** | Wire roles into surface access (`access: role:X`) + kernel middleware | v0.9.1, v0.9.7 |
Each step is CI-green independently. The first four are the high-value items.
---
## Files Affected (by component)
### Converter Consolidation (v0.9.0)
- New: `sandbox/convert.go`
- Modified: `workflow/automated.go`, `handlers/workflow_hooks.go`, `sandbox/workflow_module.go`
### Team Roles (v0.9.1)
- New: migration (PG + SQLite), `store/team_roles.go`, `handlers/team_roles.go`
- Modified: manifest validation, adoption flow, team admin UI
### Typed Forms (v0.9.3)
- New: `forms/` package (extracted from `models/workflow.go`)
- New: `sw.forms` SDK module
- Modified: workflow engine to import from `forms/` instead of inline
### Workflow Starlark Module (v0.9.5)
- Modified: `sandbox/workflow_module.go` — add start/advance/cancel/signoff
- Modified: `workflow/engine.go` — extract interface to break circular import
---
## Non-Goals
- Rewriting the workflow engine. The state machine is correct and stays.
- Adding a visual workflow builder. That's an extension concern, not kernel.
- Multi-tenancy changes. Team scoping works as designed.

View File

@@ -77,7 +77,10 @@ Every package has a `manifest.json` at its root. Example for a surface:
| `api_schema` | no | OpenAPI documentation for extension API routes (see below) | | `api_schema` | no | OpenAPI documentation for extension API routes (see below) |
| `db_tables` | no | Table definitions (see below) | | `db_tables` | no | Table definitions (see below) |
| `settings` | no | User-configurable settings schema | | `settings` | no | User-configurable settings schema |
| `exports` | libraries | Functions exported for other packages | | `exports` | no | Functions exported for cross-package calls via `lib.require()` |
| `depends` | no | Array of package IDs this package depends on |
| `slots` | no | Named UI injection points for host surfaces |
| `contributes` | no | Slot contributions into other surfaces |
| `hooks` | no | Event bus subscriptions | | `hooks` | no | Event bus subscriptions |
| `config_section` | no | Settings/Admin panel injection (see below) | | `config_section` | no | Settings/Admin panel injection (see below) |
| `capabilities` | no | Environment requirements (see below) | | `capabilities` | no | Environment requirements (see below) |
@@ -194,6 +197,117 @@ sandbox permission model.
In Starlark, check permissions inline via `req["permissions"]` or call In Starlark, check permissions inline via `req["permissions"]` or call
`permissions.check(user_id, "image-gen.use")`. `permissions.check(user_id, "image-gen.use")`.
## Extension Composability
Extensions compose with each other through three mechanisms: manifest-declared
**slots** (UI injection points), **contributions** (UI components injected into
those slots), and cross-package **function calls** via `lib.require()`.
### Slots — Host Surfaces Declare Injection Points
A surface declares named slots in its manifest where other extensions can
inject UI components:
```json
{
"id": "notes",
"type": "surface",
"slots": {
"toolbar-actions": {
"description": "Toolbar action buttons",
"context": {
"noteId": "string",
"getContent": "function — returns note body",
"setContent": "function — replaces note body"
}
}
}
}
```
The surface renders slot contents using the SDK helper:
```javascript
html`<div class="toolbar">
${sw.slots.renderAll('notes:toolbar-actions', {
noteId: note.id,
getContent: () => editor.getValue(),
setContent: (text) => editor.setValue(text),
})}
</div>`
```
### Contributions — Extensions Inject UI
An extension declares which slots it contributes to:
```json
{
"id": "note-dictate",
"type": "extension",
"contributes": {
"notes:toolbar-actions": {
"label": "Dictate",
"icon": "🎤",
"description": "Voice-to-text dictation"
}
}
}
```
In its JavaScript, it registers the component:
```javascript
sw.slots.register('notes:toolbar-actions', {
id: 'note-dictate',
priority: 200,
component: ({ noteId, setContent, getContent }) => {
// ... component implementation
return html`<button class="sw-btn sw-btn--ghost">🎤</button>`;
}
});
```
Contributions are soft-coupled — install order doesn't matter. The admin
can view all slots and contributors at **Admin > Packages** or via
`GET /api/v1/admin/slots`.
### Cross-Package Function Calls
Any package that declares `exports` in its manifest can be called by other
packages via `lib.require()`. The caller declares the dependency:
```json
{
"id": "note-ai",
"depends": ["llm-bridge"],
"permissions": ["api.http"]
}
```
In Starlark:
```python
llm = lib.require("llm-bridge")
result = llm.complete([{"role": "user", "content": prompt}])
```
The called function runs with the *target* package's permissions, not the
caller's. This is the same security model as library packages.
### Slot Naming Convention
Slot names follow `{host-package-id}:{slot-name}`. The colon separates the
namespace from the slot. Standard slots for first-party packages:
| Slot | Host | Use Case |
|------|------|----------|
| `notes:toolbar-actions` | notes | Dictation, AI tools, formatting |
| `notes:note-footer` | notes | Related items, AI summary |
| `chat:composer-tools` | chat | Image gen, file attach |
| `chat:message-actions` | chat | Reactions, translate, bookmark |
| `chat:image-actions` | chat | Regen, edit, upscale |
## Starlark Sandbox API ## Starlark Sandbox API
Starlark scripts run server-side with a 1M operation budget and no Starlark scripts run server-side with a 1M operation budget and no

View File

@@ -68,10 +68,66 @@ Only `manifest.json` is required. All other directories are optional and include
| `db_tables` | Table definitions with columns and indexes | | `db_tables` | Table definitions with columns and indexes |
| `settings` | User-configurable settings with type, label, description, default | | `settings` | User-configurable settings with type, label, description, default |
| `hooks` | Event bus subscription patterns | | `hooks` | Event bus subscription patterns |
| `exports` | Functions exported by library packages | | `exports` | Functions exported for cross-package calls via `lib.require()` |
| `depends` | Array of package IDs this package depends on |
| `slots` | Named UI injection points (host surfaces declare these) |
| `contributes` | Slot contributions this package injects into other surfaces |
| `capabilities` | Environment requirements: `{"required": [...], "optional": [...]}` | | `capabilities` | Environment requirements: `{"required": [...], "optional": [...]}` |
| `schema_version` | Integer for additive schema migrations | | `schema_version` | Integer for additive schema migrations |
## Composability: Slots and Contributions
Packages compose with each other through named UI injection points (**slots**) and **contributions**.
### Declaring Slots (Host Surfaces)
Surfaces declare slots where other extensions can inject UI:
```json
{
"slots": {
"toolbar-actions": {
"description": "Toolbar action buttons",
"context": {
"noteId": "string — current note ID",
"getContent": "function — returns note body text"
}
}
}
}
```
Slot names are namespaced at runtime as `{package-id}:{slot-name}` (e.g., `notes:toolbar-actions`). The `context` field documents what data the slot provides — it is not enforced at runtime.
### Contributing to Slots
Extensions declare which slots they inject into:
```json
{
"contributes": {
"notes:toolbar-actions": {
"label": "Dictate",
"icon": "🎤",
"description": "Voice-to-text dictation"
}
}
}
```
Contributing extensions can be installed before or after their host surface — the coupling is soft. The admin can view all slots and their contributors at `GET /api/v1/admin/slots`.
### Cross-Package Function Calls
Any package that declares `exports` can be called via `lib.require()`, not just library-type packages. The caller must declare the target in its `depends` array:
```json
{
"depends": ["image-gen"],
"permissions": ["api.http"]
}
```
## Package Lifecycle ## Package Lifecycle
1. **Install**: Upload a `.pkg` file via Admin > Packages or `POST /api/v1/admin/packages/install`. The kernel extracts the archive, creates database tables, and registers routes. 1. **Install**: Upload a `.pkg` file via Admin > Packages or `POST /api/v1/admin/packages/install`. The kernel extracts the archive, creates database tables, and registers routes.

View File

@@ -50,7 +50,7 @@ is detected by the kernel. Detected capabilities: `pgvector`, `workspace`,
### lib ### lib
Load exported functions from library packages. Load exported functions from other packages.
```python ```python
helpers = lib.require("my-utils") helpers = lib.require("my-utils")
@@ -58,11 +58,17 @@ result = helpers.format_date("2026-01-15")
``` ```
Requirements: Requirements:
- The library must be declared in your package manifest's `dependencies`. - The target package must be declared in your manifest's `depends` array.
- The library must be type `library`, status `active`, tier `starlark`. - The target package must declare `exports` in its manifest.
- The target package must be status `active`, tier `starlark`.
- Any package type (`library`, `extension`, `full`) can be called as long
as it declares exports. This enables full packages to expose callable
functions alongside their UI and API routes.
- Circular dependencies are detected and rejected. - Circular dependencies are detected and rejected.
- Results are cached per execution (calling `require` twice returns the - Results are cached per execution (calling `require` twice returns the
same object). same object).
- The called function runs with the *target* package's permissions, not
the caller's.
### permissions ### permissions

View File

@@ -0,0 +1,96 @@
package handlers
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// slotInfo describes a declared slot and its contributors.
type slotInfo struct {
Host string `json:"host"`
Description string `json:"description"`
Context map[string]any `json:"context,omitempty"`
Contributors []slotContributor `json:"contributors"`
}
// slotContributor describes one package's contribution to a slot.
type slotContributor struct {
Package string `json:"package"`
Label string `json:"label,omitempty"`
Icon string `json:"icon,omitempty"`
Description string `json:"description,omitempty"`
}
// ListSlots returns an aggregated map of all declared slots across installed
// packages together with their contributors. Built on-the-fly from manifests.
// GET /api/v1/admin/slots
func (h *PackageHandler) ListSlots(c *gin.Context) {
pkgs, err := h.stores.Packages.List(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list packages"})
return
}
slots := map[string]*slotInfo{}
// Pass 1: collect declared slots from host surfaces
for _, pkg := range pkgs {
slotsRaw, ok := pkg.Manifest["slots"].(map[string]any)
if !ok {
continue
}
for name, v := range slotsRaw {
fullName := pkg.ID + ":" + name
entry, ok := v.(map[string]any)
if !ok {
continue
}
desc, _ := entry["description"].(string)
var ctx map[string]any
if c, ok := entry["context"].(map[string]any); ok {
ctx = c
}
slots[fullName] = &slotInfo{
Host: pkg.ID,
Description: desc,
Context: ctx,
Contributors: []slotContributor{},
}
}
}
// Pass 2: collect contributions
for _, pkg := range pkgs {
if !pkg.Enabled {
continue
}
contribs, ok := pkg.Manifest["contributes"].(map[string]any)
if !ok {
continue
}
for slotKey, v := range contribs {
// Ensure the slot entry exists (contributor can arrive before host)
if _, exists := slots[slotKey]; !exists {
host := slotKey
if idx := strings.Index(slotKey, ":"); idx >= 0 {
host = slotKey[:idx]
}
slots[slotKey] = &slotInfo{
Host: host,
Contributors: []slotContributor{},
}
}
contrib := slotContributor{Package: pkg.ID}
if m, ok := v.(map[string]any); ok {
contrib.Label, _ = m["label"].(string)
contrib.Icon, _ = m["icon"].(string)
contrib.Description, _ = m["description"].(string)
}
slots[slotKey].Contributors = append(slots[slotKey].Contributors, contrib)
}
}
c.JSON(http.StatusOK, gin.H{"data": slots})
}

View File

@@ -0,0 +1,97 @@
package handlers
import (
"testing"
)
func TestValidateComposabilityFields_ValidSlots(t *testing.T) {
m := map[string]any{
"slots": map[string]any{
"toolbar-actions": map[string]any{
"description": "Toolbar action buttons",
"context": map[string]any{
"noteId": "string",
},
},
},
}
if err := ValidateComposabilityFields(m); err != "" {
t.Errorf("expected valid, got: %s", err)
}
}
func TestValidateComposabilityFields_SlotMissingDescription(t *testing.T) {
m := map[string]any{
"slots": map[string]any{
"toolbar": map[string]any{
"context": map[string]any{},
},
},
}
if err := ValidateComposabilityFields(m); err == "" {
t.Error("expected error for slot missing description")
}
}
func TestValidateComposabilityFields_SlotsNotObject(t *testing.T) {
m := map[string]any{
"slots": "bad",
}
if err := ValidateComposabilityFields(m); err == "" {
t.Error("expected error for non-object slots")
}
}
func TestValidateComposabilityFields_ValidContributes(t *testing.T) {
m := map[string]any{
"contributes": map[string]any{
"notes:toolbar-actions": map[string]any{
"label": "Dictate",
"icon": "🎤",
},
},
}
if err := ValidateComposabilityFields(m); err != "" {
t.Errorf("expected valid, got: %s", err)
}
}
func TestValidateComposabilityFields_ContributesMissingColon(t *testing.T) {
m := map[string]any{
"contributes": map[string]any{
"toolbar-actions": map[string]any{
"label": "Bad",
},
},
}
if err := ValidateComposabilityFields(m); err == "" {
t.Error("expected error for contributes key without colon")
}
}
func TestValidateComposabilityFields_Empty(t *testing.T) {
m := map[string]any{
"id": "test",
}
if err := ValidateComposabilityFields(m); err != "" {
t.Errorf("expected valid for manifest without slots/contributes, got: %s", err)
}
}
func TestValidateComposabilityFields_BothValid(t *testing.T) {
m := map[string]any{
"slots": map[string]any{
"my-slot": map[string]any{
"description": "A slot",
},
},
"contributes": map[string]any{
"other:their-slot": map[string]any{
"label": "Hello",
},
},
}
if err := ValidateComposabilityFields(m); err != "" {
t.Errorf("expected valid, got: %s", err)
}
}

View File

@@ -37,6 +37,44 @@ var validTiers = map[string]bool{
models.ExtTierSidecar: true, models.ExtTierSidecar: true,
} }
// ValidateComposabilityFields checks that manifest slots and contributes
// fields follow the expected conventions. Slots values must have a
// "description" string. Contributes keys must follow "pkg:slot" naming.
// Returns nil if valid or missing; returns an error string otherwise.
func ValidateComposabilityFields(manifest map[string]any) string {
// Validate slots: map of name → {description, ...}
if slotsRaw, ok := manifest["slots"]; ok {
slots, ok := slotsRaw.(map[string]any)
if !ok {
return "slots must be an object"
}
for name, v := range slots {
entry, ok := v.(map[string]any)
if !ok {
return "slots." + name + " must be an object"
}
if _, ok := entry["description"]; !ok {
return "slots." + name + " must have a description"
}
}
}
// Validate contributes: map of "pkg:slot" → {label, ...}
if contribRaw, ok := manifest["contributes"]; ok {
contribs, ok := contribRaw.(map[string]any)
if !ok {
return "contributes must be an object"
}
for key := range contribs {
if !strings.Contains(key, ":") {
return "contributes key " + key + " must follow pkg:slot convention"
}
}
}
return ""
}
// ── User endpoints ────────────────────────────── // ── User endpoints ──────────────────────────────
// ListUserExtensions returns all enabled extensions for the current user, // ListUserExtensions returns all enabled extensions for the current user,
@@ -208,6 +246,12 @@ func (h *ExtensionHandler) AdminInstallExtension(c *gin.Context) {
manifestMap = map[string]any{} manifestMap = map[string]any{}
} }
// Validate composability fields (slots / contributes)
if errMsg := ValidateComposabilityFields(manifestMap); errMsg != "" {
c.JSON(400, gin.H{"error": "manifest: " + errMsg})
return
}
pkg := &store.PackageRegistration{ pkg := &store.PackageRegistration{
ID: body.ExtID, ID: body.ExtID,
Title: body.Name, Title: body.Name,

View File

@@ -212,10 +212,48 @@ func (h *PackageHandler) DeletePackage(c *gin.Context) {
} }
} }
c.JSON(http.StatusOK, gin.H{"id": id, "deleted": true}) resp := gin.H{"id": id, "deleted": true}
// Warn about orphaned slot contributions
if orphaned := findOrphanedContributors(c, h.stores, id); len(orphaned) > 0 {
resp["warning"] = "slot contributions orphaned in: " + strings.Join(orphaned, ", ")
}
c.JSON(http.StatusOK, resp)
h.broadcastPackageChanged("deleted", id) h.broadcastPackageChanged("deleted", id)
} }
// findOrphanedContributors returns package IDs that declare contributes
// targeting slots owned by the given package (e.g. "notes:toolbar-actions"
// targets the "notes" package). This is a warning, not a blocker.
func findOrphanedContributors(c *gin.Context, stores store.Stores, deletedPkgID string) []string {
allPkgs, err := stores.Packages.List(c.Request.Context())
if err != nil {
return nil
}
prefix := deletedPkgID + ":"
seen := map[string]bool{}
for _, p := range allPkgs {
if p.ID == deletedPkgID || !p.Enabled {
continue
}
contribs, ok := p.Manifest["contributes"].(map[string]any)
if !ok {
continue
}
for key := range contribs {
if strings.HasPrefix(key, prefix) {
seen[p.ID] = true
}
}
}
result := make([]string, 0, len(seen))
for id := range seen {
result = append(result, id)
}
return result
}
// InstallPackage uploads and installs a .pkg/.surface archive. // InstallPackage uploads and installs a .pkg/.surface archive.
// POST /api/v1/admin/packages/install // POST /api/v1/admin/packages/install
// //
@@ -466,6 +504,13 @@ func (h *PackageHandler) parseAndValidateArchive(c *gin.Context, tmpPath string)
return nil, nil, nil, err return nil, nil, nil, err
} }
// Validate composability fields (slots / contributes)
if errMsg := ValidateComposabilityFields(manifest); errMsg != "" {
zr.Close()
c.JSON(http.StatusBadRequest, gin.H{"error": "manifest: " + errMsg})
return nil, nil, nil, fmt.Errorf("invalid composability fields")
}
// Signing hook (reserved) // Signing hook (reserved)
if os.Getenv("PACKAGE_VERIFY_SIGNATURES") == "true" { if os.Getenv("PACKAGE_VERIFY_SIGNATURES") == "true" {
if mInfo.Signature != "" { if mInfo.Signature != "" {

View File

@@ -859,6 +859,7 @@ func main() {
admin.POST("/packages/:id/update", pkgAdm.UpdatePackage) admin.POST("/packages/:id/update", pkgAdm.UpdatePackage)
admin.GET("/packages/:id/export", pkgAdm.ExportPackage) admin.GET("/packages/:id/export", pkgAdm.ExportPackage)
admin.GET("/dependencies", pkgAdm.ListAllDependencies) admin.GET("/dependencies", pkgAdm.ListAllDependencies)
admin.GET("/slots", pkgAdm.ListSlots)
// Workflow package export // Workflow package export
wfPkgH := handlers.NewWorkflowPackageHandler(stores) wfPkgH := handlers.NewWorkflowPackageHandler(stores)

View File

@@ -79,19 +79,21 @@ func BuildLibModule(ctx context.Context, r *Runner, consumerPkgID string, rc *Ru
return nil, fmt.Errorf("lib.require: package %q does not declare dependency on %q", consumerPkgID, libraryID) return nil, fmt.Errorf("lib.require: package %q does not declare dependency on %q", consumerPkgID, libraryID)
} }
// 4. Load library's PackageRegistration // 4. Load target PackageRegistration
libPkg, err := r.stores.Packages.Get(ctx, libraryID) libPkg, err := r.stores.Packages.Get(ctx, libraryID)
if err != nil { if err != nil {
return nil, fmt.Errorf("lib.require: library %q not found: %w", libraryID, err) return nil, fmt.Errorf("lib.require: package %q not found: %w", libraryID, err)
} }
if libPkg.Type != "library" { // Any package with exports is callable — not just type "library"
return nil, fmt.Errorf("lib.require: package %q is type %q, not library", libraryID, libPkg.Type) exports := extractExports(libPkg.Manifest)
if len(exports) == 0 {
return nil, fmt.Errorf("lib.require: package %q declares no exports", libraryID)
} }
if libPkg.Status != models.PackageStatusActive { if libPkg.Status != models.PackageStatusActive {
return nil, fmt.Errorf("lib.require: library %q is %s, not active", libraryID, libPkg.Status) return nil, fmt.Errorf("lib.require: package %q is %s, not active", libraryID, libPkg.Status)
} }
if libPkg.Tier != models.ExtTierStarlark { if libPkg.Tier != models.ExtTierStarlark {
return nil, fmt.Errorf("lib.require: library %q is tier %s, not starlark", libraryID, libPkg.Tier) return nil, fmt.Errorf("lib.require: package %q is tier %s, not starlark", libraryID, libPkg.Tier)
} }
// 5. Build library's module set (library's own permissions) // 5. Build library's module set (library's own permissions)
@@ -113,12 +115,7 @@ func BuildLibModule(ctx context.Context, r *Runner, consumerPkgID string, rc *Ru
return nil, fmt.Errorf("lib.require: error executing library %q: %w", libraryID, err) return nil, fmt.Errorf("lib.require: error executing library %q: %w", libraryID, err)
} }
// 8. Extract exports from manifest // 8. Build exported members (exports already extracted above)
exports := extractExports(libPkg.Manifest)
if len(exports) == 0 {
return nil, fmt.Errorf("lib.require: library %q declares no exports", libraryID)
}
members := make(starlark.StringDict, len(exports)) members := make(starlark.StringDict, len(exports))
for _, name := range exports { for _, name := range exports {
val, ok := result.Globals[name] val, ok := result.Globals[name]

View File

@@ -15,6 +15,8 @@
export function createSlots(emitFn) { export function createSlots(emitFn) {
/** @type {Map<string, Array<{id:string, component:Function, priority:number}>>} */ /** @type {Map<string, Array<{id:string, component:Function, priority:number}>>} */
const _registry = new Map(); const _registry = new Map();
/** @type {Map<string, string>|undefined} */
let _declarations;
function _sort(arr) { function _sort(arr) {
arr.sort((a, b) => a.priority - b.priority); arr.sort((a, b) => a.priority - b.priority);
@@ -73,5 +75,43 @@ export function createSlots(emitFn) {
names() { names() {
return [..._registry.keys()]; return [..._registry.keys()];
}, },
/**
* Render all components registered in a slot.
* Each component is called with the context object. Errors are
* caught so one broken contributor doesn't break the host surface.
* @param {string} name — slot name
* @param {object} context — props passed to each component
* @returns {Array} — array of rendered vnodes (nulls filtered)
*/
renderAll(name, context = {}) {
return this.get(name).map(e => {
try {
return e.component(context);
} catch (err) {
console.error(`[sw.slots] Error rendering "${e.id}" in slot "${name}":`, err);
return null;
}
}).filter(Boolean);
},
/**
* Declare a slot for discoverability. Runtime-only metadata —
* does not affect registration or rendering.
* @param {string} name — slot name
* @param {string} description — human-readable purpose
*/
declare(name, description) {
if (!_declarations) _declarations = new Map();
_declarations.set(name, description);
},
/**
* List declared slot metadata (from declare()).
* @returns {Map<string, string>}
*/
declarations() {
return _declarations ? new Map(_declarations) : new Map();
},
}; };
} }