The event bus, package registry and stabilizer stay resident and coordinate the runtime.
Documentation
Build something
the runtime can use.
Start with one small package. Give it data, an interface and clear entry points for people, agents and other apps. Lógos discovers the rest.
Lógos 2.0.0 · Contracts generated 2026-08-21 · Package contract v1
Lógos / The development library
A small kernel for software that can change while it runs.
Lógos is the independent runtime underneath ThinkIac. Point it at a packages/ folder and it discovers routes, tools, events, data models and screens. You can use that kernel to build your own product without adopting the ThinkIac interface.
HTTP, tools, views, data and processes install capabilities around the kernel and can be extended.
Your application code declares what it offers. Lógos wires those declarations into the active modules.
// server.js
import { bootstrap } from 'logos'
await bootstrap({
port: 5040,
http: { public: 'public' },
views: { index: { package: 'notes', page: 'Index' } }
})
// Drop a folder into packages/ and Lógos discovers its surfaces. Start here / Architecture target
Lógos becomes only the launcher.
The approved direction is to remove every resident capability from Lógos. The executable will only resolve a workspace boot manifest, start the declared host module and return its exit status. It will not contain a router, WebSocket server, event bus, database, vault, stabilizer or isolated runner.
packages/modules/.isolated runner is not privileged inside Lógos. A host composes it when a package declares isolated: true, just as it composes the router or WebSocket capability.Start here / Quick start
Your first package in three files.
Create packages/notes. The folder name becomes the URL namespace, while the package file names the capability people will see.
{
"name": "notes",
"version": "1.0.0",
"description": "Private notes with an agent-friendly API.",
"type": "module",
"manifest": {
"name": "Notes",
"icon": "assets/icon.svg",
"iconDark": "assets/icon-dark.svg"
}
} Save the route below. Lógos finds its comment, validates the package and publishes it at /notes/health.
// packages/notes/routes.js
export function health() {
/**
* @register_router health
* @method GET
*/
return { data: { ready: true } }
}
// GET /notes/health
// { "data": { "ready": true } } Start here / Package anatomy
Identity is declared. Capabilities are discovered.
package.json holds the package identity, icon and places where its interface may appear. Tools, hooks and routes are read from the code and can be exported as a generated logos.json snapshot when another system needs to inspect them.
What you choose
Name, version, icon, menu, InputBar, widget and runtime policies.
What Lógos finds
Tools, parameter schemas, hooks, routes and commands. Generate it; do not hand-edit it.
Icon rules
Put the files inside the package. Use icon for the light/default asset and iconDark when the dark surface needs a different one. SVG stays sharp; PNG, JPG and WebP are also accepted. If no icon exists, Aurora falls back to the package initial or Sigma.
Start here / Runtime configuration
Make lifecycle policy explicit in the manifest.
The package manifest accepts a runtime object for its ordinary contract. Critical data isolation is declared directly on package.json: isolated: true and, when required, database: "private". Package-local .env values are settings, not a vault; never commit credentials or encryption keys.
core/isolated child-process boundary.isolated: true.Build packages / Tools
Describe the decision, not the implementation.
A tool is a named function an agent can call. Its description explains when it helps; each parameter explains exactly what a valid input means. Lógos turns those declarations into the schema shown to agents and visual builders.
// packages/notes/tools.js
import { saveNote } from './lib/notes.js'
export async function createNote({ title, body, visibility = 'private', tags = [] }) {
/**
* @register_tool createNote
* @description Create a note in the private notebook
* @reach auto
* @param {string} title - Short, descriptive title
* @param {string} body - Full note content
* @param {string?} visibility - Who can read it @enum private,shared
* @param {string[]} tags[] - Labels used to find the note later
*/
return saveNote({ title, body, visibility, tags })
}
export function assignNote({ note, reviewer }) {
/**
* @register_tool assignNote
* @description Assign a note to a reviewer
* @param {string} note.id - Existing note ID
* @param {string} note.title - Title shown to the reviewer
* @param {string?} reviewer - Registered reviewer @enumSource tool:team/listMembers#id
*/
return { ok: true, noteId: note.id, reviewer }
} {string} titleRequired text{number?} limitOptional number{boolean} archivedTrue or false{string[]} tags[]List of text values{string} note.idField inside an object@enum draft,publishedClosed set of choices@enumSource tool:team/list#idChoices loaded at runtimeUse ? in the type and give the function a useful default.
@enum prevents invented values. @enumSource keeps a changing list live.
@reach auto is normal, always is always visible, and manual is only called explicitly.
Build packages / Commands
Give operators a repeatable way to act.
A command is a package function exposed to the local Lógos CLI. Use it for migrations, imports, exports and maintenance jobs that should be named, validated and testable. It receives the same declared parameters as a tool, but it is invoked deliberately by an operator instead of being offered to an agent.
// packages/notes/commands.js
import fs from 'node:fs/promises'
import path from 'node:path'
import { listNotes } from './lib/notes.js'
export async function exportNotes({ format = 'json', output = './notes.json' }) {
/**
* @register_command export
* @description Export notes to a local file
* @param {string?} format - Output format @enum json,csv
* @param {string?} output - Destination path
*/
const notes = listNotes()
const content = format === 'csv'
? ['title,body', ...notes.map(note => JSON.stringify(note.title) + ',' + JSON.stringify(note.body))].join('\n')
: JSON.stringify(notes, null, 2)
const destination = path.resolve(output)
await fs.mkdir(path.dirname(destination), { recursive: true })
await fs.writeFile(destination, content, 'utf8')
return { output: destination, records: notes.length }
}
// Discover: logos list
// Run: logos notes:export --format csv --output ./notes.csv
// JSON: logos notes:export --params '{"format":"json"}'
// Validate and swap now: logos commit notes Build packages / Scriba
Give each package its own data boundary.
Add a schema and Lógos provisions a private tenant for that package. The model becomes a lowercase collection on the proxy: Note becomes db.note.
// packages/notes/schema.prisma
model Note {
id String @id @default(uuid())
title String
body String
visibility String @default("private")
tags String?
archived Boolean @default(false)
} // packages/notes/lib/notes.js
import { getProxy } from 'logos'
const notes = () => getProxy('notes').note
export async function saveNote(input) {
return notes().push({
title: input.title.trim(),
body: input.body.trim(),
visibility: input.visibility ?? 'private',
tags: JSON.stringify(input.tags ?? []),
archived: false
})
}
export function listNotes() {
return notes().query(
{ archived: false },
{ sort: 'updatedAt DESC', limit: 50 }
)
}
export async function archiveNote(id) {
const note = notes()[id]
if (!note) return null
note.archived = true
return note
}
export async function deleteNote(id) {
await notes().delete(id)
return { ok: true }
} saveNote or listNotes. This gives every surface the same validation and behavior.What tenant isolation means
Each schema receives a separate database path and a tenant name, which prevents accidental table collisions and lets backup or rollback target one package. This is an ownership convention, not a security sandbox: trusted code can still call getProxy('other-package'). Do not install untrusted packages in the same process when data confidentiality depends on process isolation.
Build packages / Package context
Use a runtime-bound context for mediated work.
New package code should prefer container.packageContext('notes'). Its owner is bound by Lógos, so database, event, tool, file, secret, network and process calls remain attributable to the package that made them. It is an authority boundary around runtime services, not a sandbox for arbitrary JavaScript.
// Inside a module or package installer
const ctx = container.packageContext('notes')
const db = ctx.db.self()
await ctx.events.publish('notes:created', { noteId: 'n_123' })
const result = await ctx.tools.invoke('ntfy.send', { message: 'Saved' })
const config = await ctx.files.readFile('data/config.json', 'utf8')
// Never stringify a secret. The value only exists in this callback.
await ctx.secrets.get('API_TOKEN').withValue(async (token) => {
return ctx.network.fetch('https://api.example.test/status', {
headers: { authorization: `Bearer ${token}` }
})
}) withValue().isolated runs only in the Lógos child runner; the main process receives descriptors and invokes opaque handlers through IPC.Build packages / Private offline vault
Encrypt the database without depending on a cloud service.
For credentials, wallet data and other owner-only records, declare a private database. Lógos derives a separate 32-byte key for each package and opens its tenant with SQLCipher. The installation stays offline: it uses the local OS credential store when available, or an explicit password unlock that remains in memory only.
{
"name": "wallet",
"version": "1.0.0",
"isolated": true,
"database": "private"
}
// database: "private" requires isolated: true.
// Use ctx.db.self() inside the owner package.
// Share only minimized, validated data through its tools, routes or events. secret-tool on Linux. A host can supply an equivalent local keyring adapter.Authority and sharing
Only the owning package may acquire a private tenant; new package code should use ctx.db.self(). Cross-package getProxy() access is denied regardless of a declared grant. Publish a narrowly shaped tool, route or event from the owning package when another package needs a result; never share rows or encryption keys.
process, global fetch or native addons. Events and the package's own database cross the boundary only through contract-checked IPC. Files, secrets, network and processes remain denied until their dedicated mediated IPC capabilities are introduced.Build packages / Routes
Choose the route by how it behaves.
Normal routes live at /<package>/<path>. Use GET for reads, POST for creation, path parameters for one resource, @auth for protected endpoints and @upload for multipart files.
// packages/notes/routes.js
import { getProxy } from 'logos'
import { listNotes, saveNote } from './lib/notes.js'
export function list(req) {
/**
* @register_router notes
* @method GET
* @offline cache
*/
return { data: listNotes() }
}
export async function create(req) {
/**
* @register_router notes
* @method POST
* @auth
*/
const note = await saveNote(req.body)
return { status: 201, body: { data: note } }
}
export function findOne(req) {
/**
* @register_router notes/:id
* @method GET
*/
return { data: { id: req.params.id } }
}
export function update(req) {
/**
* @register_router notes/:id
* @method PATCH
* @auth
*/
const note = getProxy('notes').note[req.params.id]
if (!note) return { status: 404, body: { error: 'Note not found' } }
if (typeof req.body.title === 'string') note.title = req.body.title.trim()
return { data: note }
}
export async function remove(req) {
/**
* @register_router notes/:id
* @method DELETE
* @auth
*/
await getProxy('notes').note.delete(req.params.id)
return { data: { ok: true } }
}
export function importFile(req) {
/**
* @register_router import
* @method POST
* @upload file 1 10mb disk
*/
return { data: { name: req.file.originalname } }
} Build packages / Testing
Test plain behavior first, discovery second.
Tools, hooks and domain handlers are ordinary functions, so import and test them directly. For scanner and decorator coverage, create real named JavaScript fixtures in a temporary workspace: source comments must survive for discovery. Redirect HOME and USERPROFILE before importing modules that derive state paths, then restore globals and remove every fixture.
Run node --test packages/your-package/test/*.test.js without network access.
Run npm test for one CI pass or npm run test:watch during development.
Point registerTools, registerHooks or registerRoutes at a disposable on-disk package.
Connect & extend / Events
Use events where capabilities meet.
dispatchEvent starts a pipeline. Hooks run by priority; when one returns an object, that object becomes the payload for the next hook. This is useful for enrichment, policy and optional integrations.
// packages/notes/lib/create-note.js
import { dispatchEvent, getProxy } from 'logos'
export async function handleCreateNote(input) {
const note = await getProxy('notes').note.push(input)
await dispatchEvent('notes:created', { note })
return note
}
// packages/notes/hooks.js
export function enrichCreated(payload) {
/**
* @register_hook notes:created
* @priority 20
*/
return {
...payload,
note: { ...payload.note, searchable: true }
}
}
export function observeAllNotes(payload, context) {
/**
* @register_hook notes:*
* @priority 90
*/
console.log(context.matchedEvent, payload.note?.id)
} When one package owns the whole behavior and needs a direct result.
When other packages may observe, enrich or veto without becoming a hard dependency.
For observability across one namespace, such as notes:*.
One behavior, several entry points
Put the real work in a plain function. The route, agent tool and integration hook become small adapters around it.
// One behavior, three entry points
import { handleCreateNote } from './lib/create-note.js'
export function createFromRoute(req) {
/**
* @register_router notes
* @method POST
*/
return handleCreateNote(req.body)
}
export function createFromAgent(input) {
/**
* @register_tool createNote
* @description Create a private note
* @param {string} title - Short title
* @param {string} body - Full note content
*/
return handleCreateNote(input)
}
export function createFromIntegration(payload) {
/**
* @register_hook inbox:note-requested
*/
return handleCreateNote(payload)
} Runtime event catalog
These names and payloads come from the active Lógos, Aurora and Hermes contracts. Filter by system or search for the capability you want to observe.
22 events
runtime:readylogosemit
The HTTP server is listening and the boot checkpoint exists.
Payload{ port }export function handle(payload, context) {
/** @register_hook runtime:ready */
return payload
} package:wiringlogosemit
A package is being connected to the installed runtime modules.
Payload{ pkg }export function handle(payload, context) {
/** @register_hook package:wiring */
return payload
} route:registeringlogospipeline
Immediately before a discovered route is mounted; return skip to omit it.
Payload{ pkg, route }export function handle(payload, context) {
/** @register_hook route:registering */
return payload
} http:requestlogosemit
A request reaches any route owned by a package.
Payload{ pkg, path, method }export function handle(payload, context) {
/** @register_hook http:request */
return payload
} auth:checklogospipeline
An authenticated route asks the host to decide whether access is allowed.
Payload{ allowed, status, message }export function handle(payload, context) {
/** @register_hook auth:check */
return payload
} tool_beforelogosemit
Just before a discovered tool starts running.
Payload{ tool, payload }export function handle(payload, context) {
/** @register_hook tool_before */
return payload
} tool_afterlogosemit
After a discovered tool returns its result.
Payload{ tool, payload }export function handle(payload, context) {
/** @register_hook tool_after */
return payload
} agent:runlogospipeline
A host asks the kernel agent loop to complete a tool-using run.
Payload{ prompt, tools, model, context }export function handle(payload, context) {
/** @register_hook agent:run */
return payload
} agent:steplogosemit
Each observable step of an agent run is produced.
Payload{ type, iteration, tool, result }export function handle(payload, context) {
/** @register_hook agent:step */
return payload
} reload:validatinglogosveto
The staged candidate passed initial checks and is about to hot swap.
Payload{ pkg, checks }export function handle(payload, context) {
/** @register_hook reload:validating */
return payload
} reload:swappedlogosemit
A validated package candidate became the live version.
Payload{ pkg, checks }export function handle(payload, context) {
/** @register_hook reload:swapped */
return payload
} package:reload_failedlogosemit
Staging or validation failed and the live package stayed in service.
Payload{ pkg, error, checks, files, attempt, maxRetries }export function handle(payload, context) {
/** @register_hook package:reload_failed */
return payload
} views:file_changedlogosemit
A package view changed and browser update policy must be evaluated.
Payload{ pkg, file }export function handle(payload, context) {
/** @register_hook views:file_changed */
return payload
} db:schema_changedlogosemit
A package data schema changed and needs snapshot and validation.
Payload{ pkg }export function handle(payload, context) {
/** @register_hook db:schema_changed */
return payload
} aurora:messageaurorapipeline
A person sends a new message into an Aurora conversation.
Payload{ message, sessionId }export function handle(payload, context) {
/** @register_hook aurora:message */
return payload
} agent:skills:classifiedaurorapipeline
Aurora finishes routing AGENT.md specialists for the current request.
Payload{ sessionId, selected, pinned, status, model, latencyMs }export function handle(payload, context) {
/** @register_hook agent:skills:classified */
return payload
} agent:token:usageaurorapipeline
A model provider reports token use for a completed or streamed call.
Payload{ usage, model, provider, sessionId }export function handle(payload, context) {
/** @register_hook agent:token:usage */
return payload
} llm:callaurorapipeline
A package requests a model call from the provider pool owned by Aurora.
Payload{ system, messages, tools, model, requirements }export function handle(payload, context) {
/** @register_hook llm:call */
return payload
} device:emithermesbridge
Runtime data must reach one connected device, or all connected devices.
Payload{ event, payload, appId? }export function handle(payload, context) {
/** @register_hook device:emit */
return payload
} device:notifyhermesbridge
The runtime sends an interactive or visible notification through Hermes.
Payload{ message, title?, appId?, interaction?, discreteId?, hold? }export function handle(payload, context) {
/** @register_hook device:notify */
return payload
} device:responsehermesbridge
A paired device answers a confirm or prompt interaction.
Payload{ appId, id, action, value }export function handle(payload, context) {
/** @register_hook device:response */
return payload
} device:*hermesbridge
A paired device emits an allowed custom event into its reserved namespace.
Payload{ ...payload, appId }export function handle(payload, context) {
/** @register_hook device:* */
return payload
} Connect & extend / Runtime modules
Extend the kernel through an install contract.
A module exports a named installer decorated with @register_module and declares runtime.role: "module". @phase boot installs foundations before package wiring; @phase wire attaches runtime capabilities. Repeat @after for ordering and keep mutable state in container.state(namespace), never module globals.
Connect & extend / Process modules
Bridge another language without hiding the process boundary.
Add @runtime python, deno, ruby or bun to a module. Lógos starts it as a child and exchanges one NDJSON message per stdio line. Requests carry IDs, event names and payloads; responses correlate by ID. Declared @events patterns limit forwarded bus traffic.
Protocol only: valid single-line JSON messages. Send diagnostics to stderr.
proc:crashed records the exit and the bridge restarts the process.
Pending calls fail explicitly; callers choose retry or compensation rather than assuming success.
Connect & extend / Middleware & predicates
Compose policy around handlers.
@register_predicate names a boolean decision over request context. @register_middleware wraps routes, tools and hooks in its package and may select a predicate with @predicate. Put authentication composition, authorization, audit context and rate limiting here when the policy spans multiple entry points.
Connect & extend / Agent Runtime
Run bounded tool loops with observable results.
runAgentTask() executes one tool-calling task. Restrict exposure with toolNames, bound tool iterations with maxSteps, and validate structured final output with outputSchema. Each transition emits an agent:step trace event.
import { runAgentTask } from 'logos'
const result = await runAgentTask({
system: 'You are a financial analyst.',
goal: 'Calculate this month balance.',
toolNames: ['wallet_summary'],
maxSteps: 6,
outputSchema: {
type: 'object',
properties: {
balance: { type: 'number' },
status: { type: 'string' }
},
required: ['balance', 'status']
},
maxOutputRetries: 2
})
if (!result.success) throw new Error(result.error)
console.log(result.output, result.trace) success, text or parsed output, model, error when present, trace and step count.maxOutputRetries only retries invalid outputSchema responses. There is no generic tool/step retry or timeout option.Sequential chains
Use runChainTask() with the default runtime container. The lower-level runChain(container, steps, initial) form is for callers that already own a container. Step inputs accept {{initial.topic}}/{{results.step.text}} templates or a function receiving the full context. A failed step stops the chain and later steps are absent from results.
import { runChainTask } from 'logos'
const context = await runChainTask([
{
name: 'research',
input: 'Research {{initial.topic}}',
toolNames: ['web_search'],
maxSteps: 5
},
{
name: 'summary',
input: ctx => `Summarize:\n${ctx.results.research.text}`,
maxSteps: 3
}
], { topic: 'quantum computing' })
console.log(context.results.summary.text)
// Low-level API: runChain(container, steps, initial) Connect & extend / AGENT.md
Give Aurora expertise only when it is useful.
An AGENT.md is a package-owned instruction guide. Aurora reads its front matter, keeps truly universal guides pinned and asks its router which of the remaining guides match the current message. A narrow description makes that decision reliable.
---
name: Notes guide
description: Use when a request involves notes, notebooks, tags or archiving.
language: en
always: false
event: notes:agent-context
---
You help operate the private notebook.
{#if notes.length > 0}
There are {{ notes.length }} active notes.
{#each notes as note, i}
{{ i + 1 }}. {{ note.title }}
{/each}
{:else}
The notebook is empty.
{/if}
Never expose a private note without an explicit request. true only for rules every conversation needs; otherwise leave it false.How classification works
always: true skips relevance classification. Other guides become candidates identified by name and description; the selected files are rendered again for each prompt. The routing choice may be cached briefly, but dynamic event data is not reused.
// packages/notes/hooks.js
export async function provideAgentContext(payload) {
/** @register_hook notes:agent-context */
const { sessionId, message, agent } = payload
return {
notes: await listNotesForSession(sessionId),
request: message,
agent
}
} Inflect / Dynamic instructions
Render current context without turning the guide into code.
Inflect runs only after an AGENT.md is selected. Expressions insert values; conditions choose guidance; loops summarize small collections; switches select a mode; includes keep reusable rules in package-local partials.
---
name: Notes guide
description: Use for notes, notebooks, tags, review and archiving.
event: notes:agent-context
---
{#include ./partials/safety}
{#if user.canRead}
The notebook has {{ notes.length }} active notes.
{#each notes as note, i}
{{ i + 1 }}. {{ note.title }} — {{ note.visibility }}
{/each}
{:else if user.signedIn}
The account is not allowed to read this notebook.
{:else}
Ask the person to sign in before reading notes.
{/if}
{#switch request.mode}
{#case 'summary'}Return a short summary.{/case}
{#case 'review'}Check each note for a next action.{/case}
{:default}Follow the person's explicit request.{/default}
{/switch} .md or .mdjs partial.Connect & extend / Hermes
Extend the event bus to a paired device.
Hermes is an authenticated live link between a Machine and its devices. Use it when the app must react to runtime data or return a structured answer. Use ntfy when the goal is simply to alert the person.
device:emitRuntime → connected app. Live data is not queued because stale events can mislead the device.
device:notifyRuntime → person. Can ask for confirmation or text and supports an offline queue.
device:responseDevice → runtime. Carries confirm, cancel or submitted text back to a hook.
device:*Device → runtime custom events. The device cannot emit protected system namespaces.
import { dispatchEvent } from 'logos'
// Live data for the app. This is not a visible notification.
await dispatchEvent('device:emit', {
event: 'notes:sync-complete',
payload: { changed: 3 },
appId: 'phone_42' // optional; omit to reach all connected devices
})
// A question the person can answer from the device.
await dispatchEvent('device:notify', {
message: 'Archive the reviewed notes?',
interaction: 'confirm',
appId: 'phone_42'
}) export function handleDeviceAnswer(payload) {
/** @register_hook device:response */
const { appId, id, action, value } = payload
if (action === 'confirm') return archiveReviewedNotes({ appId, requestId: id })
if (action === 'submit') return saveDeviceText({ appId, value })
return payload
}
export function handleDeviceEvent(payload) {
/** @register_hook device:notes-opened */
// Device-originated custom events are restricted to device:*.
return { ...payload, observedAt: Date.now() }
} Trust the token
The first registration creates a device token. Every later socket must present the current token; unpairing invalidates it and closes its links.
First registration is open
An unused app ID can currently be claimed before an authenticated web session approves it. Design sensitive flows with that limitation in mind.
Connect & extend / Views
A screen is just another package surface.
Place Index.svelte or Home.svelte in views/ for the entry screen. Other files become named pages at /view/notes/PageName. Fetch the same routes agents and integrations use.
<script>
import { onMount } from 'svelte'
let notes = []
let loading = true
onMount(async () => {
const response = await fetch('/notes/notes')
const json = await response.json()
notes = json.data ?? []
loading = false
})
</script>
<main>
<h1>Notes</h1>
{#if loading}
<p>Loading…</p>
{:else}
{#each notes as note}
<article><h2>{note.title}</h2><p>{note.body}</p></article>
{/each}
{/if}
</main> think:theme, use the shared tokens and clean up browser listeners when the view closes.Connect & extend / Advanced views
Choose how pages compile, refresh and survive offline.
Use .svx for MDSvex/Markdown pages when the optional compiler is installed. The view engine follows the complete import graph for HMR, including first-party ES module helpers. Page reload modes are always, prompt and manual; a views:hmr broadcast describes changes but does not force every page to reload.
window.LogosOffline.purge() so authenticated cached responses are removed.Connect & extend / Dynamic UI
Contribute to Aurora without editing Aurora.
The visual manifest places package-owned UI into stable slots. Aurora discovers each contribution through the app catalog, so a package can be installed or removed without adding conditionals to the chat shell.
{
"manifest": {
"name": "Notes",
"icon": "assets/icon.svg",
"iconDark": "assets/icon-dark.svg",
"padding": 10,
"homeShortcut": { "name": "My Notes" },
"menu": {
"id": "notes",
"label": "Notes",
"target": "right",
"view": "Widget"
},
"inputbar": {
"id": "new-note",
"kind": "action",
"label": "New note",
"hint": "Save an idea without leaving the chat",
"view": "QuickAdd"
},
"widget": {
"lang": "notes",
"title": "Notes",
"view": "MessageWidget",
"aliases": ["note"],
"partialMin": 0
},
"messageActions": [{
"id": "listen",
"kind": "audio",
"label": "Listen",
"endpoint": "speak",
"roles": ["assistant"],
"order": 15
}]
}
} Sidebar panel
Opens the package view in the chosen desktop/mobile side.
Composer control
An action opens a view or emits an event. A state changes how the next message is handled.
MessageList card
A fenced block such as notes becomes a live package view inside the response.
Action below a reply
Use the supported audio contract with a readiness endpoint and the correct media response.
Named package slots
Any host package can create an optional extension point by reading a qualified name such as email-manager.account-connect. Providers declare manifest.slots; the app catalog publishes only installed, active package contributions. An absent provider resolves to an empty list, so hosts need no install-time dependency or fallback branch.
// Provider package.json
{
"name": "gmail-bridge",
"manifest": {
"slots": {
"email-manager.account-connect": {
"id": "google",
"view": "Connect",
"href": "/gmail-bridge/oauth/start",
"label": "Connect Google",
"order": 100,
"contract": 1
}
}
}
}
// Host: GET /aura-system/apps/list, collect app.slots,
// then select slot === "email-manager.account-connect".
// No provider installed means an empty list. slot + id. Contributions are ordered by order; if the identity repeats, the last active package wins and the resolved record names the package it overrides. A slot controls discovery and presentation, never backend authorization.Build a MessageList widget
The source block arrives in the URL fragment. Decode it, render the compact result, report the content height and use the UI bus to open the full app.
// packages/notes/views/MessageWidget.svelte
<script>
import { onMount, tick } from 'svelte'
let source = ''
function post(type, detail) {
parent.postMessage({ source: 'aurora', type, detail }, '*')
}
async function reportHeight() {
await tick()
post('widget:height', document.documentElement.scrollHeight)
}
onMount(() => {
const params = new URLSearchParams(location.hash.slice(1))
const encoded = params.get('data')
const payload = encoded
? JSON.parse(decodeURIComponent(escape(atob(encoded))))
: {}
source = payload.source ?? ''
reportHeight()
})
</script>
<button on:click={() => post('open:app', '/view/notes/Index')}>
Open notes from “{source}”
</button> Send an InputBar or widget event to the backend
Events starting with logos: cross the chat bridge. Other UI events, such as open:app and open:iframe, stay in the shell.
// A package view runs in an iframe and talks to Aurora with postMessage.
parent.postMessage({
source: 'aurora',
type: 'logos:notes:refresh',
detail: { noteId: 'note_42' }
}, '*')
// The backend receives the same event through a normal hook.
export function refreshNote(payload) {
/**
* @register_hook logos:notes:refresh
*/
return { ok: true, noteId: payload.noteId }
} ThinkIac platform / Standards
Use these conventions when your package lives in ThinkIac.
Lógos runs standalone and does not require these visual conventions. ThinkIac packages do: read think:theme, apply data-theme, use the shared --tk-* tokens, avoid hard-coded colors and make full-screen views 100dvh. Use inline SVG icons rather than emoji, flat surfaces, restrained opacity/transform motion and the shared radius tokens.
storage listener when a fullscreen view sits outside the shell.Package collaboration
Depend on a capability, not another package's internals.
Packages can collaborate through events, tools or HTTP routes. Events are the loosest contract: a provider may be installed later, multiple packages can observe the request, and the caller can require an explicit handled result. Direct imports and cross-tenant proxies create a hard dependency and should be reserved for packages deployed and versioned together.
// Prefer a capability contract over another package's files.
import { dispatchEvent } from 'logos'
export async function assignReviewer(noteId) {
const result = await dispatchEvent('team:reviewer-requested', {
handled: false,
noteId
})
if (!result.handled) throw new Error('No reviewer provider is installed')
return result.reviewer
}
// Direct data access is technically possible, but it couples trust and schema:
// getProxy('other-package').record
// Package tenants are namespaces, not a security sandbox. Optional provider, policy pipeline, observation or veto.
Agent-facing capability with a generated input contract; call it programmatically only when that coupling is intentional.
Network boundary, authentication, independent deployment or external consumer.
Shared code released as one unit. Declare the dependency in package.json.
Structural dependencies
Use logosDependencies only when a named Lógos package must exist for yours to work. Append ? to make a provider optional. Required dependencies that cannot resolve, validate, install, or escape a cycle leave the consumer blocked; it is not wired until the graph is healthy. The host chooses the registry, stages packages and verifies npm dist.integrity before they enter the workspace.
{
"name": "notes",
"logosDependencies": {
"@thinkiac/embeddings": "1.4.0",
"@thinkiac/ntfy?": "^1.0.0"
}
}
// Required dependency unavailable or cyclic → package state: blocked
// Optional dependency unavailable → package can still start Operate safely / Stabilizer
Save first. Become live after validation.
When package code changes, Lógos prepares a candidate on a ghost router beside the running version. It checks that routes mount, declared tools load, required tools exist, smoke routes return successfully and no reload:validating hook vetoes the change. Only then does one atomic router assignment make it live.
If the candidate fails, the working package keeps serving and package:reload_failed records the error, checks and changed files. Every stable state receives a checkpoint; repeated failure restores the package and its database snapshot together. Tests are not run automatically unless you expose them as a smoke route or enforce them with a reload:validating hook.
Operations
Observe the bus, then enforce the limits your product needs.
Use http:request, tool_before, tool_after, agent:step and reload events for structured logs and traces. The observability package persists execution history without changing the package being observed.
// Observe without changing the package being observed.
export function traceTools(payload) {
/** @register_hook tool_after */
console.log(JSON.stringify({
tool: payload.tool,
sessionId: payload.sessionId,
result: payload.payload
}))
return payload
}
export function reportReloadFailure(payload) {
/** @register_hook package:reload_failed */
console.error(payload.pkg, payload.error, payload.checks)
return payload
} Data evolution
Change shape in reversible steps.
A schema.prisma edit is validated before the candidate runs and the tenant database is snapshotted before the swap. That protects rollback, but it does not invent business migrations or safely backfill required values for you.
import { getProxy } from 'logos'
// 1. Back up the tenant data before changing schema.prisma.
// 2. Add nullable/defaulted fields before making them required.
// 3. Save the schema; Lógos validates it and snapshots .database.
// 4. Backfill old records with a one-time command.
// 5. Tighten the schema only after the backfill is verified.
export async function backfillVisibility() {
/**
* @register_command migrate:visibility
* @description Backfill visibility on records created before version 1.1
*/
const notes = getProxy('notes').note.query({ visibility: { $null: true } })
for (const note of notes) note.visibility = 'private'
return { changed: notes.length }
} For a failed candidate, inspect package:reload_failed; for repeated failures, inspect package:rolled_back. Keep independent backups outside the Machine for disaster recovery—the Stabilizer checkpoint is operational recovery, not a substitute for backup retention.
API & troubleshooting
Start from the small public surface.
import {
bootstrap, // start a runtime around packages/
dispatchEvent, // run an event pipeline
getProxy, // access a named Scriba tenant
command, // run a discovered package command
broadcast, // send an event to connected browser sessions
runAgentTask, // run the host-provided agent loop
generateManifest, // inspect discovered capabilities
requestReboot // calculate impact before a full restart
} from 'logos' logos list; confirm the exported function is named and its decorator is inside or immediately above it.package:reload_failed and the stability checks; the rejected candidate never replaced live code.Lógos capability manifests describe the active package contract. Pin compatible versions of the kernel and packages, read release notes before upgrades, and treat removed exports, decorator behavior or payload changes as breaking changes.
Operate safely / Stabilizer operations
Control when an edit transaction becomes live.
The Stabilizer groups file changes until quiescence, then stages and validates one candidate. Commit immediately with the runtime_commit tool, the reload:commit pipeline or a package .commit marker. A new valid package is wired live and emits package:added; it does not require a process restart.
package:stopped.Operate safely / HTTP reference
Read every endpoint as a complete contract.
/_logos/offlineNo auth · no body · computed offline policy · 200 JSON; 500 when policy assembly fails./logos-sw.jsNo auth · Service Worker JavaScript · 200; excluded from its own cache scope./view/:package/:pagePackage page · compiled HTML · 404 for an unknown view; build errors remain explicit./:package/:routeContract comes from @method, @auth, upload/stream tags and the handler return shape.Protected package routes accept Authorization: Bearer <token>. JSON writes use Content-Type: application/json. Return { status, headers, body } for explicit errors and document package-specific payloads beside their handler.
Operate safely / Reference index
Exports, decorators, events and endpoints in one index.
This compact catalog is generated from a structured local source so navigation and search use the same names. The linked canonical contracts remain authoritative for detailed payloads.
Generated from 12 active exports, decorators and event contracts · compatible with ^2.0.0
bootstrap()Start and configure a Lógos runtime.
dispatchEvent() / $()Run a pipeline and return its transformed payload.
runAgentTask() / runChainTask() / runChain()Run one agent loop or a sequential chain; the low-level chain receives a container.
@register_routerPublish an HTTP or WebSocket handler.
@register_toolPublish an agent-callable function.
@register_moduleInstall or override a runtime module.
@register_predicate / @register_middlewareCompose package-local policy around handlers.
agent:stepObserve each trace step of an agent run.
reload:commitRequest an explicit Stabilizer commit.
package:reload_failedObserve a rejected candidate and its checks.
GET /_logos/offlineInspect the computed offline policy.
GET /logos-sw.jsServe the policy-bearing Service Worker.
Operate safely / Production readiness
Verify the whole operating boundary before launch.
Terminate modern TLS, restrict exposed ports and define request/body limits.
Keep package credentials out of Git, rotate them and limit provider scopes.
Store encrypted off-machine copies and rehearse tenant and host recovery.
Collect structured request, tool, agent and reload events with retention and alerts.
Set route, tool, storage and provider limits at enforceable boundaries.
Run untrusted or critical workloads outside the shared runtime process.
Ship & license / Upgrade guide
Upgrade contracts before implementations.
Record the current Lógos and package versions, read release notes, compare generated manifests and identify breaking changes in exports, decorators, event payloads, views and database schemas. Take an external backup and a deploy checkpoint, upgrade in staging, run package plus runtime suites, then promote with rollback criteria already written.
Ship & license / Auto-Repository
Your Machine has inspectable history.
Each Machine can expose its working tree as a repository. Clone it, branch, push and connect a deployment to a specific change. Runtime edits become reviewable history instead of hidden service state.
Clone, change, deploy and recover
Add only your public SSH key in ThinkIac. The gateway authenticates it and forwards Git operations to the correct Machine; it never receives your private key. A pushed branch creates a deployment candidate tied to that commit. Promotion activates the already-built release, while rollback reactivates the previous immutable release.
# 1. Add your public SSH key in ThinkIac, then clone the Machine.
git clone git@thinkiac.space:ada/my-machine.git
cd my-machine
# 2. Make an isolated branch and verify the package.
git switch -c feature/notes-sharing
npm test -- --test-name-pattern notes
git add packages/notes
git commit -m "Add shared notes"
# 3. Push. The branch receives its own deployment candidate.
git push -u origin feature/notes-sharing
# 4. Review its URL and logs in Git Cloud, then Promote.
# Production moves to this exact commit; it is not rebuilt.
# 5. If health regresses, choose Rollback in Git Cloud.
# The previous immutable release becomes active again. Ship & license / Two projects
Open kernel. Source-visible platform.
Lógos and ThinkIac are related, but they are not licensed as one product. Choose the boundary that matches what you want to build, and read the complete license before commercial deployment.
MIT open source
Use, modify, distribute and embed the independent runtime kernel under the permissive MIT license.
Read the MIT license →TSAL source-available
The platform source can be studied, modified and run under the ThinkIac Source-Available License. It is not an OSI-approved open source license; commercial and competing-product restrictions apply.
If your organization reaches US$100,000 or R$500,000 in consolidated annual gross revenue, a commercial license is required for each fiscal year in which it reaches that threshold.
Ship & license / Self-hosting
Your infrastructure, the same package contract.
Run a Machine on a supported Linux host or a Docker-capable workstation. You operate the host, network, backups and updates; packages, routes, tools and data boundaries behave the same way. Start by deciding whether systemd or a container owns the process—do not run both for the same workspace.
Node.js 18+, a non-root service account, a persistent workspace volume and a DNS name when the service is public.
Keep runtime and package variables in protected environment files. Never commit tokens, database keys or TLS private keys.
Expose only the reverse proxy. Terminate TLS there, set request limits and forward the original host and protocol headers.
Mount package data, Scriba databases, checkpoints and uploads on durable storage outside an ephemeral container layer.
Encrypt and copy data off-machine, retain multiple points, and test restoring into an isolated Machine.
Stage a version, health-check it, promote it, and retain the last known-good release plus a written rollback command.
git clone <your-machine-repository> thinkiac
cd thinkiac
docker compose up -d
# Open the runtime, enroll the Machine, then keep secrets in
# the package .env or the host secret manager — never in source code. Systemd service
[Unit]
Description=ThinkIac Machine
After=network-online.target
[Service]
User=thinkiac
WorkingDirectory=/srv/thinkiac
EnvironmentFile=/etc/thinkiac/runtime.env
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target Reverse proxy and operations
Proxy HTTPS to the local Machine port, preserve Host and X-Forwarded-Proto, and keep the application port bound to loopback. Before every upgrade: take an encrypted backup, record the active versions, deploy to staging, run npm test, check /__health, then promote. Restore by stopping the service, restoring the verified data snapshot to a separate path, validating it, and switching back only after the health check passes.
Ship & license / Security boundaries
State the protection that is actually running.
A plain container, a hardened host and a sandboxed workload do not offer the same isolation. ThinkIac keeps that boundary explicit instead of turning every deployment into the same marketing promise.