golem-kit 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +11 -6
  3. package/docs/agents.md +64 -0
  4. package/docs/app-backend.md +259 -0
  5. package/docs/architecture.md +93 -0
  6. package/docs/builder.md +15 -0
  7. package/docs/knowledge.md +35 -0
  8. package/docs/local-cli.md +31 -15
  9. package/docs/source-development.md +31 -0
  10. package/index.html +9 -0
  11. package/package.json +24 -5
  12. package/src/backend/accounts.ts +287 -0
  13. package/src/backend/app.ts +269 -0
  14. package/src/backend/files.ts +68 -0
  15. package/src/backend/http.ts +276 -0
  16. package/src/backend/index.ts +10 -0
  17. package/src/backend/jobs.ts +302 -0
  18. package/src/backend/jsonl.ts +87 -0
  19. package/src/backend/knowledge.ts +264 -0
  20. package/src/backend/model.ts +129 -0
  21. package/src/backend/rules.ts +53 -0
  22. package/src/backend/sqlite.ts +73 -0
  23. package/src/backend/views.ts +216 -0
  24. package/src/brain.ts +94 -0
  25. package/src/browser/adapters.ts +229 -53
  26. package/src/browser/ansi.ts +104 -0
  27. package/src/browser/app.d.ts +5 -2
  28. package/src/browser/app.tsx +167 -39
  29. package/src/browser/groups.tsx +29 -0
  30. package/src/browser/main.tsx +1 -0
  31. package/src/browser/panekeys.ts +34 -0
  32. package/src/browser/sources.tsx +113 -0
  33. package/src/browser/styles.css +36 -0
  34. package/src/browser/terminal.tsx +89 -0
  35. package/src/browser-build.ts +20 -7
  36. package/src/chat.ts +74 -0
  37. package/src/cli.ts +91 -17
  38. package/src/client.ts +205 -0
  39. package/src/config.ts +169 -0
  40. package/src/dev-server.ts +339 -38
  41. package/src/entry.mjs +19 -0
  42. package/src/eslint.mjs +55 -0
  43. package/src/operations.ts +169 -0
  44. package/src/runtime/assistant.ts +141 -0
  45. package/src/runtime/discovery.ts +13 -7
  46. package/src/runtime/harness/agent-status.js +388 -0
  47. package/src/runtime/harness/claude-tmux.js +573 -0
  48. package/src/runtime/harness/codex-notify.js +95 -0
  49. package/src/runtime/harness/codex-tmux.js +292 -0
  50. package/src/runtime/harness/fake.js +430 -0
  51. package/src/runtime/harness/package.json +1 -0
  52. package/src/runtime/harness/port.js +208 -0
  53. package/src/runtime/harness/tmux-session.js +556 -0
  54. package/src/runtime/harness/tmux.js +285 -0
  55. package/src/runtime/harness/turnend-hook.js +105 -0
  56. package/src/runtime/session.ts +171 -34
  57. package/src/runtime/tmux.ts +173 -0
  58. package/src/runtime/tool-names.ts +19 -0
  59. package/src/source-mode.ts +56 -0
  60. package/vite.config.ts +2 -4
  61. package/src/runtime/codex.ts +0 -119
package/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ - `golem-kit/server` export and an `exports` map, so an app imports the backend by name instead of by path.
6
+ - App backend: records, files and server-defined operations, documented in `docs/app-backend.md`.
7
+ - App server code can call a model: `context.model.extract` takes free text and returns a schema-shaped value; the app chooses the model, and reads images too.
8
+ - Durable scheduled and long-running app jobs, with their runs visible in the sample app.
9
+ - Optional local accounts, groups and build access: invites, open sign-up, guests refused from `invoke`, signed-out and demoted pages cleared live.
10
+ - App brain: a `brain/` Open Knowledge Format bundle served read-only, a Brain reader beside the app, and agent citations rendered as source chips.
11
+ - Knowledge files and consented source views: sources offered from chat, opened only on consent, drafts kept across switching and closing, symlinked roots refused.
12
+ - Ordinary chat: an API agent limited to the app's listed operations, configured apart from the builder.
13
+ - Chat belongs to a role: `chat.roles` gates every chat route, and build keeps its own permission. No chat means no chat rights.
14
+ - Tmux chat provider: one persistent agent session per app (`golem-<app>`), `golem say` to post back into it, `/reset`, and resume of legacy harness refs into `golem-<app>:builder`.
15
+ - Normal-mode tmux chat runs read-only, with a launch profile per window replayed on resume.
16
+ - Terminal popup in build mode: a live tmux pane stream, with keys going straight to the pane.
17
+ - Browser shell bottom bar: the app / Brain / Admin in the menu row, Builder and theme in the gear; an app with more than one screen gets its own items.
18
+ - Builder mode is app state, with a Shell toggle and an app-defined normal chat.
19
+ - Build-mode chat: a Stop pill via `chat.interrupt`, and a one-row compact header at 320px chat width.
20
+ - Build mode reloads the page only when the bundle changed, and paints the theme before the first frame.
21
+ - Persistent dark mode toggle, and the framework's dark styles packaged.
22
+ - Source mode covers the app's own code, not just the shell (`GOLEM_SOURCE`, `GOLEM_UI_SOURCE`).
23
+ - Claude Code runs builds beside Codex; a new conversation picks its agent.
24
+ - App server code reloads after build-mode rebuilds, and the last good module is kept when an edit is malformed.
25
+ - Shared architecture lint (`golem-kit/eslint`, `./golem lint`) and an App DNA template in new apps.
26
+ - New apps install `golem-ui` directly, pinned to the version golem-kit builds with.
27
+ - Requires `golem-ui` 0.2.0.
28
+
29
+ ## 0.1.1
30
+
31
+ First published release: the local CLI, the dev server, the browser shell and the Codex build session.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What is Golem?
6
6
 
7
- Golem is an early local application shell for building applications with AI. **Your app is its own IDE:** enter build mode, ask a locally authenticated Codex to edit the app, and keep using it in the same browser shell.
7
+ Golem is an early local application shell for building applications with AI. **Your app is its own IDE:** enter build mode, ask a locally authenticated Claude Code or Codex to edit the app, and keep using it in the same browser shell.
8
8
 
9
9
  ## Getting Started
10
10
 
@@ -22,20 +22,25 @@ Start the application:
22
22
  ./golem dev
23
23
  ```
24
24
 
25
- Open the browser address printed in the terminal, enter build mode, and start a conversation. Build chat requires the Codex CLI installed and authenticated on the same machine. After a successful build-mode change, the browser shell rebuilds and refreshes; conversations are saved in `.golem/` and restored when the server restarts.
25
+ Open the browser address printed in the terminal, enter build mode, and start a conversation. Build chat requires the Claude Code or Codex CLI installed and signed in on the same machine; when both are ready, choose one before entering build mode. Each conversation keeps its agent; use **New conversation** to start another, possibly with the other agent, while earlier conversations stay saved. After a successful build-mode change, the browser shell rebuilds and refreshes; conversations are saved in `.golem/` and restored when the server restarts.
26
26
 
27
- `./golem build` writes the browser shell to `dist/`. `./golem dev` binds only to `127.0.0.1:3000`.
27
+ `./golem build` writes the browser shell to `dist/`. `./golem dev` defaults to
28
+ `127.0.0.1:3000`; set an optional `host` and `port` in `golem.config.ts`, then restart
29
+ the dev server. The terminal prints the usable address.
30
+
31
+ `./golem lint` checks the app's architecture boundaries; see
32
+ [the architecture guide](docs/architecture.md) to adapt or disable them.
28
33
 
29
34
  ## Local source development
30
35
 
31
- Use an installed package by default, or opt into a durable checkout while developing Golem:
36
+ Use an installed package by default, or put a durable checkout in the app's uncommitted `.env.local` while developing Golem:
32
37
 
33
38
  ```sh
34
- GOLEM_SOURCE=/path/to/golem ./golem dev
39
+ GOLEM_SOURCE=/path/to/golem
35
40
  ```
36
41
 
37
42
  See [the local CLI guide](docs/local-cli.md) for the complete command contract.
38
43
 
39
44
  ## Current scope
40
45
 
41
- Claude integration, domain storage, accounts, and permissions are planned, not part of this release.
46
+ Apps store records and files and define server operations as described in [the app backend guide](docs/app-backend.md). Builds run with Codex or Claude Code; see [the local CLI guide](docs/local-cli.md). An app can also offer ordinary chat: an assistant that uses the app's data as the person chatting, configured apart from the builder; see [the agents guide](docs/agents.md).
package/docs/agents.md ADDED
@@ -0,0 +1,64 @@
1
+ # Agents
2
+
3
+ A Golem app has two kinds of agent. They are configured separately and never share access.
4
+
5
+ | | Builder | Ordinary chat |
6
+ |---|---|---|
7
+ | Purpose | Change the app's code | Use the app: find, read and change its data |
8
+ | Who may start one | Anyone without accounts; with accounts, managers and the `builder` role | Anyone who may use the app |
9
+ | Runs as | This computer's account (Codex or Claude Code CLI) | The person chatting, through the app's operations |
10
+ | Can reach | The file system, like any terminal agent | Only the operations the app lists |
11
+ | Credential | The CLI's own sign-in | `ANTHROPIC_API_KEY` in the server environment |
12
+
13
+ ## Configuration
14
+
15
+ ```ts
16
+ // golem.config.ts
17
+ export default {
18
+ title: 'Field Notes',
19
+ agents: {
20
+ builder: 'claude', // or 'codex': the agent build mode starts with
21
+ ordinary: {
22
+ backend: 'anthropic',
23
+ operations: ['records.list', 'records.get', 'records.update', 'notes.archive'],
24
+ collections: ['notes'],
25
+ instructions: 'Notes belong to field teams. Quote a note title before changing it.',
26
+ },
27
+ },
28
+ }
29
+ ```
30
+
31
+ - `builder` is the default choice in build mode. A person's own pick in the browser still wins and is remembered.
32
+ - `ordinary` turns on the **Start a chat** button. Leave it out and there is no ordinary chat.
33
+ - `ordinary.model` defaults to `claude-opus-5`.
34
+ - `golem.config.ts` is bundled into the browser. Keep the API key in `.env.local` or the server environment, never in this file.
35
+
36
+ ## What ordinary chat enforces
37
+
38
+ - **Operations**: the agent gets a tool for each listed operation and nothing else. It cannot build, run commands, read files, or enter build mode. A message cannot add a tool.
39
+ - **Permissions**: each tool call goes through the same `authorize` as a browser call, as the person who sent the message, refreshed on every call. A call the person may not make fails with the same `Not allowed: <operation>` error the app's own UI would get. Signing out stops a running turn.
40
+ - **Collections and roots**: with `collections`, the built-in `records.*` operations refuse any other collection; with `roots`, the `knowledge.*` operations and source offers refuse any other knowledge root. Custom operations are limited only by being listed and by `authorize`; `collections` does not confine what their code touches.
41
+ - **Refused**: a terminal backend (`'codex'` or `'claude'`) for ordinary chat, because its file access cannot be limited to the listed operations. Also `files.upload` and `files.read`, which carry file bytes, and operations whose tool names would be invalid or the same (`a.b` and `a__b` both become `a__b`). Golem stops at startup instead of ignoring the setting.
42
+
43
+ ## Showing sources
44
+
45
+ With knowledge roots (see `knowledge.md`) and `view.actions` and `view.request` listed in `operations`, the assistant can offer to open a knowledge file at a passage. The offer appears under the chat in the tab the message came from, with **Open** and **Dismiss**. Nothing opens until the person chooses **Open**; then that tab alone shows the file in the Editor, headed by its path and the passage's line numbers. Other tabs on the same sign-in are not moved.
46
+
47
+ - **Marking the passage needs a newer golem-ui.** The shell hands the Editor the passage through its `focus` prop: the lines, the file version they were counted in, and their text. The Editor waits until it has that version or a later one and no conflict is open. It then marks the passage where that exact text appears once in what it shows, so unsaved edits above it are allowed for. It marks nothing when the text is missing or appears twice, and it never changes the draft. The released golem-ui 0.1.1 has no `focus`: the file opens at the top, and the header's saved line numbers are the only pointer. To get the mark before a release, run against a golem-ui checkout that has it, with `GOLEM_UI_SOURCE` (see `source-development.md`).
48
+ - **Edits are kept.** Opened sources share one Editor for the life of the page. Opening another source parks the current one's unsaved draft or open conflict in the Editor without writing it, and opening it again restores it. **Back to app** hides the Editor, and **Show** under the chat brings it back as it was. A header note names any other source with unsaved edits. Leaving or reloading the page while a source has unsaved edits, a save in flight or an open conflict asks the browser to confirm first.
49
+
50
+ ## Conversations
51
+
52
+ - An ordinary chat belongs to the account that started it, or, for a signed-out visitor where the app allows guests, to that browser (an opaque `HttpOnly` cookie). Nobody else can read or continue it, managers included.
53
+ - Chats are saved in `.golem/` with the model's context, tool results included, and restored after a restart. Nothing runs again by itself: a turn cut off by a restart or **Interrupt** stays stopped, and a tool call whose outcome is unknown is reported to the model as unknown.
54
+ - **Interrupt** during a tool call lets that call finish; the model is not called again for that turn.
55
+ - Provider failures appear in the chat as a plain message. They never include the key.
56
+
57
+ ## Provider
58
+
59
+ Ordinary chat calls the Anthropic Messages API through `@anthropic-ai/sdk` with a manual tool loop: each listed operation is a client tool (`.` becomes `__`, since tool names must match `^[a-zA-Z0-9_-]{1,128}$`), and each call is answered with a `tool_result`, marked `is_error` when the operation refuses.
60
+
61
+ - Tool use: https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview and https://platform.claude.com/docs/en/agents-and-tools/tool-use/define-tools
62
+ - Models (`claude-opus-5`): https://platform.claude.com/docs/en/about-claude/models/overview
63
+
64
+ The tests run against a scripted stand-in for this API, `test/fixtures/messages-api.mjs`; point `ANTHROPIC_BASE_URL` at it to try chat without a key.
@@ -0,0 +1,259 @@
1
+ # App backend
2
+
3
+ Golem serves an app's records, files and operations from the same local server as the browser shell.
4
+ Records are plain JSON values; the store adds `id`, `version`, `createdAt` and `updatedAt`.
5
+ Business rules stay in the app; this package supplies storage, the operation boundary and the browser binding.
6
+
7
+ ## Where code lives
8
+
9
+ | Path | Runs in | Imports |
10
+ | --- | --- | --- |
11
+ | `src/app.tsx` and other UI files | browser | `golem-ui`, `golem-kit/client`, `src/shared/` |
12
+ | `src/shared/` | both | plain types and values only |
13
+ | `src/server/index.ts` | server | `golem-kit/server`, `src/shared/`, `src/server/**` |
14
+ | `src/server/persistence/` | server | the only place for backend-specific code (`records.native`) |
15
+
16
+ UI code reaches data only through `golem-kit/client`. Secrets come from `process.env` (loaded from `.env.local`) inside `src/server/`; `golem.config.ts` is bundled into the browser, so it holds no secrets.
17
+
18
+ ## Configuration
19
+
20
+ `golem.config.ts` keeps `title`, `host` and `port` and adds one optional field:
21
+
22
+ ```ts
23
+ export default { title: 'Field Notes', storage: 'sqlite' } // 'jsonl' (default) or 'sqlite'
24
+ ```
25
+
26
+ Data lives under `.golem/data/`: `records/<collection>.jsonl` or `records.sqlite`, plus `files/<id>`. Switching `storage` starts from an empty store; there is no migration between the two.
27
+
28
+ - **jsonl**: one append-only log per collection, fsynced per write and compacted on load. A torn final line from a crash is dropped. One server process per app.
29
+ - **sqlite**: one `records` table of JSON documents through the built-in `node:sqlite`, queried with `json_extract`. `records.native` is the open `DatabaseSync` for real SQL in `src/server/persistence/`.
30
+
31
+ ## Browser binding: `golem-kit/client`
32
+
33
+ ```ts
34
+ import { files, invoke, records } from 'golem-kit/client'
35
+ <RecordList config={...} adapters={{ records }} />
36
+ <Upload config={{ folder: 'attachments' }} adapters={{ files }} />
37
+ const result = await invoke<{ message: string }>('notes.archive', { id })
38
+ ```
39
+
40
+ `records` and `files` implement the golem-ui `RecordsAdapter` and `FilesAdapter`. A stale `update` (one with `expectedVersion`) rejects with golem-ui's `VersionConflictError`, which carries the current row. The released golem-ui 0.1.1 `RecordForm` sends only the changed fields and no `expectedVersion`, so the last save wins; call `records.update` with `expectedVersion` yourself where that matters. The next golem-ui `RecordForm` sends the loaded record's `version` as `expectedVersion` and lets the person choose between their values and the saved ones; until it is released, use it through `GOLEM_UI_SOURCE` (see `source-development.md`). `subscribe` listens to the server's change stream, so writes from any caller, the agent included, refresh open lists.
41
+
42
+ ## Server module: `src/server/index.ts`
43
+
44
+ Optional. When the file exists it must default-export an `AppServerModule`:
45
+
46
+ ```ts
47
+ import { defineOperation, z, type AppServerModule } from 'golem-kit/server'
48
+
49
+ export const archive = defineOperation({
50
+ name: 'notes.archive',
51
+ description: 'Archive one note so it leaves the active list.',
52
+ input: z.object({ id: z.string() }),
53
+ output: z.object({ id: z.string(), version: z.number() }),
54
+ record: (input) => ({ collection: 'notes', id: input.id }),
55
+ async run(input, { records }) {
56
+ const note = await records.update('notes', input.id, { archived: true })
57
+ return { id: note.id, version: note.version }
58
+ },
59
+ })
60
+
61
+ export default {
62
+ operations: [archive],
63
+ authorize: ({ operation, record }) => !(record?.locked === true && operation !== 'records.get'),
64
+ } satisfies AppServerModule
65
+ ```
66
+
67
+ Exact signatures (source: `src/operations.ts`, `src/backend/app.ts`):
68
+
69
+ ```ts
70
+ type AppServerModule = {
71
+ operations?: Operation[]
72
+ jobs?: JobDefinition[] // see Jobs
73
+ authorize?: (request: AuthorizeRequest) => boolean | Promise<boolean>
74
+ resolvePrincipal?: (request: IncomingMessage) => Principal | Promise<Principal>
75
+ }
76
+ type AuthorizeRequest = { operation: string; input: unknown; principal: Principal; via: 'http' | 'agent' | 'server'; record: Row | null }
77
+ type Principal = { kind: 'anonymous' } | { kind: 'user'; id: string; name: string; roles: string[]; groups: string[]; session?: string }
78
+ type OperationContext = { principal; via; records: RecordStore; files: FileStore; model: Model; permits(record: Row): Promise<boolean>; job?: JobContext }
79
+ type Model = { extract<S extends z.ZodType>(request: { schema: S; text: string; instructions?: string; images?: string[] }): Promise<z.output<S>> }
80
+ ```
81
+
82
+ - **One invoke path.** HTTP (`POST /api/app/operations/<name>`), the raw file routes and in-process agent tools (`app.agentTools(principal)`) all call the same `invoke`: validate input, load the `record` the operation names, `authorize`, run, validate output.
83
+ - **Principal** comes from the server, never from request input: local accounts when `accounts` is configured (see below), else `resolvePrincipal`. Without either, every caller is `anonymous` and `authorize` defaults to allowing everything: the local single-person mode existing apps run in.
84
+ - **authorize** runs once per call with the target `record` (or `null`), and again per row for `records.list` and `files.list`, where `false` hides the row. App operations that return lists filter with `context.permits(row)`; `context.records` and `context.files` are unfiltered.
85
+ - **Builtin operations** back the adapters and go through the same hook: `records.list|get|create|update|remove`, `files.list|upload|read|caption|remove`. File metadata lives in the internal `_files` collection, so `authorize` sees it as `record` for file reads and writes.
86
+
87
+ ## Calling a model
88
+
89
+ `context.model.extract` turns free text into a value one of the app's own schemas accepts:
90
+
91
+ ```ts
92
+ const meeting = await model.extract({ schema: z.object({ date: z.string(), attendees: z.array(z.string()) }), text, instructions: 'Leave a field empty rather than guessing.' })
93
+ ```
94
+
95
+ The app names the shape it wants and never which model answered. Today that is the local Claude
96
+ Code CLI — the runtime build mode already depends on, and the one that asks for no API key; the prompt
97
+ goes in on stdin and the call runs in a temporary directory, so neither `ps` nor the app's folder
98
+ is part of it. When no model can answer — the runtime is missing, times out, or gives nothing the
99
+ schema accepts — it throws `ModelUnavailableError`. Catch it and store the failure as a state of
100
+ the record: the text a person sent is worth keeping whether or not the structure came back.
101
+
102
+ Photos go in next to the text as `images: string[]` — absolute paths on the server's disk, up to
103
+ 10, read by the runtime itself and never copied. A runtime that reads no images throws
104
+ `ModelUnavailableError`; a path that is missing, unreadable or not absolute is `InvalidError`.
105
+
106
+ ## Knowledge files
107
+
108
+ Markdown the app keeps in its own folders, committed to Git with the code. Opt in from `src/server/index.ts`; only trusted server code names the folders:
109
+
110
+ ```ts
111
+ export default {
112
+ knowledge: { handbook: 'knowledge' }, // root name → directory inside the app
113
+ authorize: ({ operation, principal, record }) =>
114
+ !operation.startsWith('knowledge.') || !record?.path?.startsWith('staff/') || principal.groups.includes('staff'),
115
+ } satisfies AppServerModule
116
+ ```
117
+
118
+ That adds four operations on the usual invoke path. `authorize` sees `record = { id: '<root>/<path>', root, path }` on every call, including for a file not created yet, and again per file for `list` and `search`, where `false` hides it.
119
+
120
+ | Operation | Input | Result |
121
+ | --- | --- | --- |
122
+ | `knowledge.list` | `{ root, folder? }` | `{ rows: [{ id: path, path, title, type, version }] }` |
123
+ | `knowledge.search` | `{ root, text, limit? }` | `[{ path, line, text }]`, lines are 1-based |
124
+ | `knowledge.read` | `{ root, path }` | `{ id: path, root, path, body, version, sha256, type, title }` |
125
+ | `knowledge.write` | `{ root, path, body, expectedVersion }` | the file as read; `expectedVersion: 0` creates |
126
+
127
+ - **Paths** are relative `.md` paths. `..`, hidden segments, absolute paths and backslashes are refused. Nothing between the app root and a file may be a symlink, the configured root included, and a file with a second hard link is refused, so each file has exactly one path. File text is data for the reader and the agent, never instructions to Golem.
128
+ - **Versions**: `version` counts the contents Golem has seen, kept in the internal `_knowledge` collection. An edit made on disk shows up as a new version on the next read or save. A write sent with an older version is refused with `VersionConflictError` carrying the current file, so nothing is overwritten.
129
+ - **Limit**: Golem's own writes are serialized per file and the disk bytes are compared again right before an atomic rename. An editor outside Golem is not locked: a save from it that lands in that last instant is overwritten. One server process per app.
130
+ - `type` and `title` come from the file's frontmatter (see [the knowledge recipe](knowledge.md)); `title` falls back to the first `#` heading, then the file name.
131
+
132
+ In the browser, `knowledge` from `golem-kit/client` is a golem-ui `RecordsAdapter`: `collection` is the root name, `id` the path, `body` the text. Use it with `Editor` (versioned saves; a conflicting save is merged line by line) and with `RecordList` for navigation:
133
+
134
+ ```tsx
135
+ <Editor config={{ collection: 'handbook', id: 'guides/opening.md' }} adapters={{ records: knowledge, clock }} />
136
+ ```
137
+
138
+ ## Showing a source in the person's view
139
+
140
+ An agent can offer to open a knowledge file with a passage highlighted. The person accepts or dismisses the offer; nothing moves until they accept, and only the view where they accepted moves.
141
+
142
+ - **Conversations** belong to the agent runtime. It installs `app.views.useConversations({ owner(request, principal), owns(conversation, owner) })`: `owner` derives a trusted key from the real request and its server-resolved principal (an account id, or a key for the runtime's own browser cookie, which is how anonymous visitors are told apart), `null` to refuse; `owns` says whether that key owns the conversation. Until they are installed, no view opens and no offer is made.
143
+ - **View**: each browser tab showing a conversation calls `openView(conversation, listener)` from `golem-kit/client`. The server issues an unguessable view id bound to the tab's principal and sign-in session, its owner key and that conversation. Every later call on the view checks all three again.
144
+ - **Message**: before accepting a chat message that names a view, the runtime calls `app.views.bound(view, { principal, owner, conversation })` and refuses the message when it is `false`.
145
+ - **Offer**: the runtime builds the agent's tools with `app.agentTools(principal, { owner, conversation, view })`, `view` being the one the message came from. With knowledge configured, this adds `view.actions` (the catalog, no data) and `view.request { action: 'source.open', input: { root, path, quote? | line?, endLine? } }`; the same call is `app.views.request({ principal, owner, conversation, view }, action, input)`. The server reads the file as the principal first; a denied or missing file refuses with the same `Cannot open that source`. The offer `{ id, conversation, input: { root, path, line, endLine } }` goes to that one view. Without `view` it is only returned (`delivered: false`) for the chat to show.
146
+ - **Answer**: `answer(offer, true)` from the view checks the person and the file again, then sends `apply` to that view only. If the file changed since the offer, the offered lines are found again where they now are; if they are gone, the answer fails with `VersionConflictError` and the agent has to offer again. Consent is per offer; an unanswered offer expires after ten minutes, and a closed tab's view is dropped. Render the file with `Editor` and move to `line`–`endLine`. `apply` carries the file `version` those lines were counted in and their `text`. Accepting may be what notices a change on disk, so give `Editor` the `focus` once the file it shows has that version, or the passage is marked in the text it is about to replace. When the person has unsaved edits the lines on screen differ from the file's: find `text` in the draft and focus there, or open without a mark when it is not there.
147
+
148
+ ## Accounts
149
+
150
+ Optional local accounts: email and password, server sessions, roles and groups. Without `accounts` in `golem.config.ts` the app stays anonymous and nobody signs in.
151
+
152
+ ```ts
153
+ export default {
154
+ title: 'Field Notes',
155
+ accounts: {
156
+ guests: false, // default: signed-out visitors see only the sign-in screen
157
+ allowSignUp: false, // default: people join through invite links
158
+ roles: [ // golem-ui Auth roles; ids are unique
159
+ { id: 'member', label: 'Member' },
160
+ { id: 'builder', label: 'Builder' },
161
+ { id: 'admin', label: 'Admin', manages: true },
162
+ ],
163
+ },
164
+ origin: 'https://notes.example.test', // only when served behind a proxy or HTTPS; see below
165
+ }
166
+ ```
167
+
168
+ The roles above are the default. A role with `manages: true` may invite, change roles and groups, remove members, and build. The `builder` role may build. Every other role is for the app's own `authorize`. At least one role must manage, and the last member holding one cannot be demoted or removed. An invite carries its role. A sign-up without one (`allowSignUp: true`) gets the first role that neither manages nor is `builder`, whatever the order.
169
+
170
+ - **guests: false**: `invoke` refuses `anonymous` with `UnauthorizedError` on every path (HTTP, agent tools, server code). Signed-out `/api/app/*` calls, the change stream included, answer 401, and the shell shows golem-ui's sign-in screen.
171
+ - **guests: true**: signed-out callers run as `anonymous` through `authorize`. The default `authorize` allows everything, so write one that refuses what guests may not do.
172
+ - **Policy** stays in `authorize`: check `principal.roles`, `principal.groups` and `record`. Without an `authorize`, every signed-in member may do everything.
173
+ - **Build mode** needs a signed-in member who may build; `/api/runtime` and every `/api/sessions` route answer 401 or 403 to anyone else. A build conversation belongs to the member who started it. Conversations saved before accounts were enabled are visible to managers only. Losing build access, or signing out everywhere, interrupts a running build turn.
174
+ - **Managing**: managers get a Admin item in the shell menu row: golem-ui's member list for invites, roles and removal, plus a groups editor. Apps can use `identity` and `setGroups` from `golem-kit/client`.
175
+ - **Identity in the UI**: `identity` from `golem-kit/client` is golem-ui's `IdentityAdapter`. Pass it to `Auth.Guard` or `Timeline`. It exposes nothing a server rule trusts.
176
+
177
+ ### First admin and recovery
178
+
179
+ When the store has no account yet, `./golem dev` prints a one-use admin invite link that expires in 24 hours. Open it and sign up. There are no default credentials. A running app never creates another admin link by itself. If every admin is locked out, stop the server and run `GOLEM_ADMIN_INVITE=1 ./golem dev`. It prints a fresh admin invite link to the terminal only. Anyone who can start the server already owns `.golem/data`.
180
+
181
+ ### Agents and jobs
182
+
183
+ - `app.agentTools(principal)` refreshes the principal before every call. A principal with a `session` works only while that browser session is live; after sign-out its calls fail with `UnauthorizedError` and never fall back to anonymous.
184
+ - Server-owned work that acts for a person (a job; see [Jobs](#jobs)) stores the account id and calls `app.resolveAccount(id)` on every run. It gets the current roles and groups, no session, and `ForbiddenError` once the account is removed. Nothing a browser sends becomes a principal.
185
+
186
+ ### Security boundary
187
+
188
+ - Passwords are hashed with Node's scrypt and a random salt.
189
+ - The session cookie is a random 256-bit token. It is `HttpOnly` and `SameSite=Lax`, is `Secure` when `origin` is https, and expires after 14 days. The server stores only its SHA-256 in the reserved `_sessions` collection. Sign-out and member removal delete it.
190
+ - Accounts, sessions and invites live in reserved `_` collections. The records operations refuse those collections, and the change stream never names them.
191
+ - Five failed sign-ins lock that email, and separately that client address, for 15 minutes.
192
+ - Browser writes must come from this origin. Without `origin`, the `Origin` header must match the `Host` header. Behind a proxy, set `origin` to the public origin; then it is the only one accepted. Forwarding headers such as `X-Forwarded-For` are never read, so behind a proxy the per-address lockout counts the proxy's address.
193
+ - This protects one app's data between people who use it. It is not a hosted identity provider: there is no email verification, password reset (a manager removes and re-invites), external sign-in or two-factor. Server code and anyone with the data directory can read everything.
194
+
195
+ ## Jobs
196
+
197
+ Optional server-side work that keeps running when the browser closes: one run now, or on a schedule. The app owns each job; a caller picks a job and its input, never the operation or who it acts for.
198
+
199
+ ```ts
200
+ // src/server/index.ts
201
+ export default {
202
+ operations: [generate],
203
+ jobs: [{ name: 'sample-notes', description: 'Write a batch of sample notes.', operation: 'notes.generate' }],
204
+ } satisfies AppServerModule
205
+ ```
206
+
207
+ ```ts
208
+ type JobDefinition = {
209
+ name: string
210
+ description: string
211
+ operation: string // a registered app operation
212
+ anonymous?: boolean // guests may start it when the app has accounts; runs act as anonymous
213
+ missed?: 'skip' | 'once' // default 'skip'
214
+ }
215
+ ```
216
+
217
+ From the browser, `jobs` in `golem-kit/client` wraps the builtin operations: `jobs.start(job, input)`, `jobs.schedule(job, { every: seconds } | { cron, timezone }, input)`, `jobs.unschedule(id)`, `jobs.runs({ job?, scheduleId?, limit? })`, `jobs.cancel(id)`, `jobs.resolve(id, 'retry' | 'dismiss')`, `jobs.list()` and `jobs.subscribe(listener)`. Each is an ordinary operation (`jobs.start` and so on), so it goes through `authorize`. For `jobs.cancel`, `jobs.resolve` and `jobs.unschedule`, `authorize` also gets the run or schedule as `record`.
218
+
219
+ - **Who a run acts for.** A signed-in caller's account id is stored with the run or schedule. Every run calls `app.resolveAccount(id)`, so it gets that account's current roles and groups and needs no live session. Then it calls the operation through `invoke` with `via: 'server'`. If the account was removed or lost a role, the run fails with that error. Anonymous callers run as `anonymous`. In an app with accounts, they need `anonymous: true` on the job and `guests: true`.
220
+ - **Visibility.** People see, cancel and settle only their own runs and schedules, and `authorize` can narrow that further. Run state lives in the reserved `_job_runs` and `_job_schedules` collections, which `records.*` refuse. The change stream names only `_jobs`.
221
+ - **Operation context.** A run passes `context.job = { runId, key, signal, progress }` to the operation. Call `await job.progress({ done, total, message })` to report progress. `key` stays the same across an explicit retry and is unique for each scheduled slot, and it is a valid record id. Write with ids derived from it (`${key}-${index}`) so a retry skips work already done. Golem makes no exactly-once promise.
222
+ - **Cancel** aborts `job.signal`, and nothing more. The run stays `running`, with `cancelRequested`, until the operation returns or throws. Pass the signal on (`fetch`, `timers/promises`) or call `signal.throwIfAborted()` between steps. An operation that ignores the signal finishes as `succeeded`. Writes already made stay.
223
+ - **Statuses:** `running`, `succeeded`, `failed` (with `error`), `cancelled` and `interrupted`.
224
+ - **Stopping the server** aborts every running operation's `signal` and records nothing more. An operation that ignores the signal keeps going until the process exits, and calls it already made to other services may still complete.
225
+ - **Restarts.** A run still `running` when the server stopped becomes `interrupted` at the next start. Its effects may be partial, so it never runs again by itself. `jobs.resolve(id, 'retry')` starts a new run with the same input and `key`, and `'dismiss'` leaves it as is. Only one retry is accepted per interrupted run.
226
+ - **Schedules** persist across restarts. `every` is in seconds, 10 or more, counted from when the previous slot fired. `cron` uses five fields, or six with seconds, parsed by [croner](https://github.com/hexagon/croner), and needs an IANA `timezone`, such as `'Europe/Lisbon'`.
227
+ - **Overlap** is skipped. A slot is skipped and recorded as `lastSkippedAt` while an earlier run of the schedule is still running (cancelling included) or is interrupted and not yet settled. An unsettled interrupted run pauses its schedule.
228
+ - **Missed slots** are slots that passed while the server was down. With `skip`, the schedule waits for the next slot and records `lastMissedAt`. With `once`, it runs one catch-up at start.
229
+ - **Reload** validates `jobs` with the rest of the module. A bad definition keeps the old module serving. A schedule or interrupted run remembers the operation it was made with. If its job is removed or now names another operation, it never runs. The schedule records the reason as `error` and skips each slot. To pick up the change, schedule the job again.
230
+ - Finished runs are kept; there is no retention limit yet.
231
+ - Jobs do not send notifications, export data or call external services unless the app's own operation does.
232
+
233
+ ## Errors
234
+
235
+ Throw these from `golem-kit/server`; the HTTP status and the browser error follow from the name.
236
+
237
+ | Error | Status | Meaning |
238
+ | --- | --- | --- |
239
+ | `InvalidError` | 400 | Bad input, name, id, folder or path segment |
240
+ | `UnauthorizedError` | 401 | Not signed in, or the session ended |
241
+ | `ForbiddenError` | 403 | `authorize` returned false |
242
+ | `NotFoundError` | 404 | No such record, file or operation |
243
+ | `VersionConflictError` | 409 | `expectedVersion` no longer matches; carries `current` |
244
+ | `RecordRefusedError` | 422 | A business rule refused the write; `fields` name the controls |
245
+ | `ModelUnavailableError` | 503 | No model could answer `context.model` |
246
+
247
+ Any other thrown error answers 500 with a generic message and is logged on the server.
248
+
249
+ ## Server code reload
250
+
251
+ After a successful build-mode turn, the dev server rebuilds the browser, then bundles `src/server/index.ts` with every local file it imports and swaps the new operations and hooks in place. Stores, open change streams and conversations continue. Packages stay shared with the running server. If the new module fails to load or validate, the old one keeps serving and the conversation shows the error instead of refreshing. Outside build mode, restart `./golem dev` after editing server code.
252
+
253
+ ## Adding a capability
254
+
255
+ 1. Put shared record shapes in `src/shared/`.
256
+ 2. Define the operation in `src/server/`, naming its `record` when it acts on one, and add it to `operations`.
257
+ 3. Add any access rule to `authorize` in terms of `principal`, `operation` and `record`.
258
+ 4. Call it from the UI with `invoke('<name>', input)`; use `records` and `files` for plain CRUD and uploads.
259
+ 5. Exercise it in the browser: create, edit, reload the page, restart `./golem dev`, and confirm the data is still there.
@@ -0,0 +1,93 @@
1
+ # App architecture
2
+
3
+ How a Golem app keeps its knowledge in the right place, and how `./golem lint` checks the few
4
+ boundaries Golem cares about.
5
+
6
+ ## Knowledge ownership
7
+
8
+ Each piece of knowledge has one owner:
9
+
10
+ - **The app** owns its business: `docs/domain.md` (the app's DNA) and the code under `src/`.
11
+ Concepts, operations, rules, and decisions specific to this app live only here.
12
+ - **golem-kit** owns the shell, CLI, runtime, and this generic guidance. Read it from the
13
+ installed package; the app keeps pointers to it, never copies.
14
+ - **golem-ui** owns components and their adapter contracts. Its `README.md` is the reference.
15
+
16
+ When a change needs reusable framework or UI-kit behavior, propose it for that package instead
17
+ of growing a private copy inside the app.
18
+
19
+ ## DNA and code change together
20
+
21
+ `docs/domain.md` records the app's purpose, concepts, operations, and decisions. When a change
22
+ adds or alters a concept, an operation, or a rule, update the DNA in the same change as the
23
+ code. Discuss changes to an important decision with the person before implementing them.
24
+
25
+ ## Layout
26
+
27
+ Golem assumes only this much structure under `src/`:
28
+
29
+ | Path | Holds | May import |
30
+ | --- | --- | --- |
31
+ | `src/` (outside `src/server/`) | Browser UI and domain logic | `src/shared/`, browser-safe packages |
32
+ | `src/shared/` | Plain types and values used by both sides | Nothing from `src/server/` |
33
+ | `src/server/` | Backend modules | Anything except storage drivers |
34
+ | `src/server/persistence/` | Storage adapters | Storage drivers |
35
+
36
+ Everything else, such as module names and folder depth, is the app's choice. Record choices that
37
+ matter in `docs/domain.md`.
38
+
39
+ ## Deliberate seams
40
+
41
+ The UI reaches the backend through the app's declared API, never by importing server modules.
42
+ Types the UI and server share go in `src/shared/`, so type-only imports from `src/server/` are
43
+ refused too. Components receive data through golem-ui's `config` plus `adapters` shape rather
44
+ than opening their own data connections. Storage drivers stay behind persistence adapters, so the
45
+ rest of the app depends on the adapter's contract, not on a database client.
46
+
47
+ ## Checking the architecture
48
+
49
+ `./golem lint` runs ESLint with the app's `eslint.config.mjs`. The generated file uses the shared
50
+ config:
51
+
52
+ ```js
53
+ import golem from 'golem-kit/eslint'
54
+
55
+ export default golem()
56
+ ```
57
+
58
+ It reports:
59
+
60
+ - storage driver imports (`pg`, `mysql2`, `better-sqlite3`, `node:sqlite`, `mongodb`, `redis`,
61
+ and similar) outside `src/server/persistence/`;
62
+ - imports of `src/server/` modules, `golem-kit/server`, or `node:` built-ins from code outside
63
+ `src/server/`.
64
+
65
+ These are architectural guardrails an app may adapt, not security boundaries. They read import
66
+ specifiers as written: relative paths are matched by directory name, and aliases, bare Node
67
+ built-in names such as `fs`, `require()`, and dynamic `import()` are not checked. Keep secrets on
68
+ the server and out of browser bundles regardless of what the lint reports.
69
+
70
+ ## Adapting or disabling a rule
71
+
72
+ Change a rule deliberately and say why in the same change. Pass options to `golem()`:
73
+
74
+ ```js
75
+ import golem, { golemDrivers } from 'golem-kit/eslint'
76
+
77
+ export default golem({
78
+ server: 'src/backend', // backend directory; false disables the server boundary
79
+ persistence: 'src/backend/storage', // storage adapter directory
80
+ drivers: [...golemDrivers, 'example-db'], // false disables the driver boundary
81
+ serverModules: ['golem-kit/server'], // packages only the server may import
82
+ })
83
+ ```
84
+
85
+ For a single justified exception, use ESLint's standard directive with a reason:
86
+
87
+ ```ts
88
+ // eslint-disable-next-line no-restricted-imports -- migration script reads the legacy database directly
89
+ import pg from 'pg'
90
+ ```
91
+
92
+ To turn every Golem check off, replace `golem()` with your own ESLint config, such as
93
+ `export default []`. An app created before this config existed can opt in by adding the `eslint.config.mjs` above.
@@ -0,0 +1,15 @@
1
+ # Building a Golem app
2
+
3
+ You are the in-app builder. The person sees a Chat beside their app’s Canvas. Ask what they want to create in ordinary language, then help them shape it. Build mode is explicit: only build-mode turns may change files. After a successful build-mode turn, Golem rebuilds and refreshes the Canvas. Conversation history and sessions persist, so continue the work naturally when a thread resumes.
4
+
5
+ Read `docs/domain.md`, the app's DNA, first, and update it in the same change as the code it describes. `node_modules/golem-kit/docs/architecture.md` explains where code belongs; run `./golem lint` to check it. Before a major design or build, discuss the app’s intent, important workflows, and contracts with the person. Keep their business language, rules, and decisions in this app; put reusable framework behavior and UI-kit changes in their owning packages. Keep plumbing separate from business decisions and make each module own the knowledge it needs.
6
+
7
+ For implementation and pull-request work, start with the user or business problem and the resulting behavior. Keep a PR description to one short paragraph; add terse validation and dependencies only when useful. Run checks appropriate to the change.
8
+
9
+ Supported app surface: edit `src/app.tsx` (it may also `export const screens = [{ id, label, icon? }]`; each one gets an item in the bottom menu row and the chosen id arrives as the `screen` prop); configure the shell title, host, port, storage, optional accounts, and optional agents in `golem.config.ts` (read `node_modules/golem-kit/docs/agents.md` before turning on ordinary chat); use `./golem help`, `./golem build`, `./golem lint`, and `./golem dev`. The installed `golem-ui` package is the component contract: its `README.md` names the current components and adapters, while its API and adapter docs are linked there. Use its `config` plus `adapters` shape rather than inventing a data layer inside a component. Ask before changing an important application contract or proposing framework/UI-kit work.
10
+
11
+ Before storing records or files, adding server behavior an agent or the UI calls, or adding sign-in, roles or groups, read `node_modules/golem-kit/docs/app-backend.md`: it names where UI, shared, and server code live and the operation, authorization, and storage contracts.
12
+
13
+ Before adding a knowledge base, notes, a handbook or any markdown the app should keep in folders, read `node_modules/golem-kit/docs/knowledge.md`.
14
+
15
+ When the app has a `brain/` folder, it is an Open Knowledge Format bundle: read `brain/index.md` first, then the concepts it points to. Ground answers in those files and cite each passage you used as `path#Lstart-Lend`, the path relative to `brain/` (`concepts/opening.md#L4-L9`); Golem turns a citation in your message into a source chip that opens the passage in the Brain reader. Keep `brain/index.md` and `brain/log.md` current when you add or change a concept.
@@ -0,0 +1,35 @@
1
+ # Knowledge recipe
2
+
3
+ Read when an app keeps markdown knowledge in folders: a handbook, research notes, a second brain. The storage, permission and editing contracts are in [the app backend guide](app-backend.md#knowledge-files); this file is how to organize and maintain the content. It is optional per app, and the app chooses its folders and types.
4
+
5
+ ## Format: OKF v0.2
6
+
7
+ Files follow the [Open Knowledge Format v0.2](https://github.com/GoogleCloudPlatform/open-knowledge-format/blob/ad30107c31c06aec8a7d5636e0d1058118604e6f/SPEC.md) (pinned; v0.2 renamed fields from v0.1). Golem reads only `type` and `title` and preserves everything else.
8
+
9
+ - One concept per `.md` file. Its path without `.md` is its identity, so rename with care and fix the links that point at it.
10
+ - Each concept starts with YAML frontmatter holding a non-empty `type`. `title` and a one-sentence `description` are recommended. The app picks its own type names.
11
+ - Link concepts with ordinary markdown links, preferably from the root: `[Opening the workshop](/guides/opening.md)`. A link to a page not written yet is fine.
12
+ - `index.md` and `log.md` are reserved. `index.md` lists a folder's concepts under headings, one `* [Title](path) - description` line each, with no frontmatter (the root `index.md` may carry `okf_version: "0.2"`). `log.md` records changes newest first under `## YYYY-MM-DD` headings.
13
+
14
+ ```markdown
15
+ ---
16
+ type: Playbook
17
+ title: Opening the workshop
18
+ description: What to switch on, in order, before the first job of the day.
19
+ ---
20
+ # Opening the workshop
21
+
22
+ 1. Unlock the side door.
23
+ 2. Switch on the dust extractor before any saw.
24
+ ```
25
+
26
+ ## Maintaining it
27
+
28
+ - **Agree the map first.** Before writing much, settle with the person which folders and types the domain needs. Add folders when material asks for them.
29
+ - **Capture, then organize.** New material goes in as it arrives, even rough. Tidy it in small steps: split a concept that covers two things, merge duplicates, add the links.
30
+ - **Keep sources.** Record where a claim came from with a link, or keep the original under `references/`, so an answer can be traced back to its source.
31
+ - **Keep indexes current.** When you add, rename or remove a concept, update the `index.md` of its folder in the same change and add a `log.md` line.
32
+ - **Answer from sources.** Search, read the files, and cite the path and lines you used. Say what is missing or looks out of date instead of filling the gap; offer to write it down.
33
+ - **Edit with the reader.** Read before writing and send the version you read. When a save is refused because the file changed, read it again and merge; never write over the other change.
34
+
35
+ Frontmatter such as `verified` or `status` is advisory. Who may read or change a file is decided only by the app's `authorize`.