@happyvertical/smrt-app-shell 0.60.17 → 0.61.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.
- package/AGENTS.md +51 -9
- package/README.md +378 -23
- package/agents/draft-ui.md +74 -0
- package/agents/engine.md +81 -0
- package/agents/shared-pieces.md +127 -0
- package/dist/engine/app-runtime.d.ts +7 -0
- package/dist/engine/app-runtime.js +30 -3
- package/dist/engine/commands.d.ts +23 -0
- package/dist/engine/commands.js +29 -0
- package/dist/engine/draft.d.ts +31 -0
- package/dist/engine/draft.js +88 -0
- package/dist/engine/host.d.ts +175 -1
- package/dist/engine/host.js +501 -9
- package/dist/engine/index.d.ts +4 -2
- package/dist/engine/index.js +2 -1
- package/dist/engine/publish.d.ts +100 -0
- package/dist/engine/publish.js +218 -0
- package/dist/engine/store.d.ts +8 -0
- package/dist/engine/store.js +8 -0
- package/dist/engine/worker-protocol.d.ts +1 -1
- package/dist/engine/worker-protocol.js +1 -0
- package/dist/lib/objects/App.d.ts +42 -1
- package/dist/lib/objects/App.js +72 -4
- package/dist/lib/objects/AppDraft.d.ts +31 -0
- package/dist/lib/objects/AppDraft.js +73 -0
- package/dist/lib/objects/AppDraftVersion.d.ts +25 -0
- package/dist/lib/objects/AppDraftVersion.js +68 -0
- package/dist/lib/objects/Approval.d.ts +4 -4
- package/dist/lib/objects/Approval.js +3 -3
- package/dist/lib/objects/index.d.ts +2 -0
- package/dist/lib/objects/index.js +2 -0
- package/dist/lib/server/approval-store.d.ts +6 -0
- package/dist/lib/server/approval-store.js +102 -0
- package/dist/lib/server/approvals.d.ts +15 -0
- package/dist/lib/server/approvals.js +62 -43
- package/dist/lib/server/builder.d.ts +18 -10
- package/dist/lib/server/builder.js +46 -18
- package/dist/lib/server/drafts.d.ts +100 -0
- package/dist/lib/server/drafts.js +324 -0
- package/dist/lib/server/events.d.ts +14 -0
- package/dist/lib/server/events.js +3 -0
- package/dist/lib/server/host.d.ts +44 -2
- package/dist/lib/server/host.js +155 -11
- package/dist/lib/server/results.d.ts +6 -0
- package/dist/lib/server/results.js +8 -0
- package/dist/lib/server/target.d.ts +24 -0
- package/dist/lib/server/target.js +64 -0
- package/dist/lib/server/version-store.d.ts +40 -1
- package/dist/lib/server/version-store.js +144 -32
- package/dist/manifest.json +699 -6
- package/dist/smrt-knowledge.okf.md +19 -2
- package/package.json +19 -15
package/AGENTS.md
CHANGED
|
@@ -6,22 +6,30 @@ for one person: the owner. Epic #4867; this package is #4872. The engine is
|
|
|
6
6
|
documented in [agents/engine.md](agents/engine.md) (it must not import
|
|
7
7
|
SvelteKit or `src/lib`). README has the route and JSON contracts.
|
|
8
8
|
|
|
9
|
+
Shared assistant adapters and legacy approval storage: [agents/shared-pieces.md](agents/shared-pieces.md).
|
|
10
|
+
|
|
9
11
|
## Where things live
|
|
10
12
|
|
|
11
13
|
| Path | What |
|
|
12
14
|
|---|---|
|
|
13
|
-
| `src/engine/` | `AppHost`, worker thread, migrations, QuickJS sandbox. Finished and tested on its own. |
|
|
15
|
+
| `src/engine/` | `AppHost`, worker thread, migrations, QuickJS sandbox, drafts (`forkDraft`/`previewPublish`/`publishDraft`/`undoTo`, frozen host, draft-only `autoApprove`; see engine.md "Drafts"). Finished and tested on its own. |
|
|
14
16
|
| `src/lib/objects/` | Shell models (`objectsDir` in `vite.config.ts`): `App`, `AppVersion`, `Approval`, `AssistantThread`, `AssistantMessage`. |
|
|
15
17
|
| `src/lib/server/smrt.ts` | `createSmrtSvelteKitRuntime`, `prepareDatabase`, `configureShell`. |
|
|
18
|
+
| `src/lib/server/registry.ts` | The ONE startup registration path (permissions, preference kinds); `smrt.ts` imports it before the runtime. app-runtime grants newly registered permissions to an existing owner on every start. `registry.test.ts` guards it. |
|
|
16
19
|
| `src/lib/server/schema.ts` | `prepareShellSchema(db)`: additive first-run schema. |
|
|
17
|
-
| `src/lib/server/host.ts` | Holders: shell db, data root, the
|
|
18
|
-
| `src/lib/server/
|
|
20
|
+
| `src/lib/server/host.ts` | Holders: shell db, data root, the LIVE `AppHost` (`getHost`, never the draft) and the open-draft slot; `hostFor(target)` is how a request reaches either; restart adoption of an open draft (re-freeze live, reopen the draft with `autoApprove`). |
|
|
21
|
+
| `src/lib/server/target.ts`, `drafts.ts`, `view.ts` | The copy a request names (`live`/`draft`, `CallScope`), the draft lifecycle (start, describe, preview, publish and discard apply, History grouping) and the owner's saved view. README "Draft then publish (the shell)" lists routes, payloads, events. |
|
|
22
|
+
| `src/lib/server/version-store.ts` | `ShellVersionStore` (engine `VersionStore` on `AppVersion` + the `App` row; `commit` is one transaction, a publish's draft mark rides in it) and `DraftVersionStore` (`AppDraftVersion` rows of one `AppDraft`). |
|
|
19
23
|
| `src/lib/server/owner.ts` | Owner check, `Principal`, `assertOwner`, `runAs`. |
|
|
20
|
-
| `src/lib/server/approvals.ts` |
|
|
24
|
+
| `src/lib/server/approvals.ts`, `approval-store.ts` | Shared-authority storage adapter and shell action executor; the only place `approved: true` is set. |
|
|
21
25
|
| `src/lib/server/builder.ts` | `runCommand`/`runAppTool`/`runRest`/`requestRecordDelete`: execute or turn into an Approval. |
|
|
22
26
|
| `src/lib/server/catalog.ts`, `policy.ts`, `mcp-server.ts` | Merged tool catalog, the one policy, the `/mcp` server. |
|
|
23
27
|
| `src/lib/server/http.ts`, `events.ts` | Route plumbing; in-process event bus for SSE. |
|
|
24
|
-
| `src/lib/server/
|
|
28
|
+
| `src/lib/server/inference.ts`, `inference-registry.ts` | smrt-inference host + controller over shell.db and the runtime's master key; `asOwner` runs a settings request as the verified owner; `installShellInference` registers the user resolver (the assistant's key source). |
|
|
29
|
+
| `src/lib/server/assistant/`, `src/lib/assistant/` | Assistant: turn loop, hold, config, routes (`mountAssistantRoutes`), thread store; chat column UI (`AssistantPanel`), pure reducer/stream parser, `guidance/system.md`. |
|
|
30
|
+
| `src/routes/+layout.svelte`, `src/lib/ui/layout.ts`, `src/lib/ui/menu.ts` | Shell layout over smrt-svelte `AppShell`: left sidebar (the person's app only), the chat column (`panel`), footer voice/assistant toggles, user menu (Approvals with count, History, Theme, Settings, Sign out). Plugin mode (iframe or `?embed=plugin`) has no chat or toggles. Chat open state and the sidebar collapse (`edgeToggles.left`, icon rail) are per owner in localStorage; `sidebarFooterDivider={false}` removes the footer line so it does not clash with the chat composer's border. |
|
|
31
|
+
| `src/lib/server/agent-cli/`, `assistant/agent-turn.ts`, `onboarding.ts` | Thin host adapters over smrt-inference/server (CLI, token scope, shell guidance/approvals), the `assistant` preference (engine, onboarding), onboarding routes; UI `routes/welcome`, `components/ConnectAi.svelte`, `routes/settings/assistant`. |
|
|
32
|
+
| `src/routes/settings/` | Settings menu (`ShellSettingsPage`), built-in sections, owner-only `inference/` (panel + actions over the shared controller). |
|
|
25
33
|
| `src/routes/` | `/api/shell/*`, `/api/app/[...path]`, `/api/events`, `/mcp`, `/setup`, health. UI pages are placeholders. |
|
|
26
34
|
|
|
27
35
|
## Invariants
|
|
@@ -49,6 +57,18 @@ SvelteKit or `src/lib`). README has the route and JSON contracts.
|
|
|
49
57
|
reserved core endpoints are refused (only the app's own collections are
|
|
50
58
|
served). Snapshots are retained (20 `method-*`; clean up prunes old
|
|
51
59
|
`migrate-*`/`schema-*`).
|
|
60
|
+
- **Drafts and targets.** Every request names its copy; there is no ambient
|
|
61
|
+
"current host". REST: `?target=` (`x-shell-target` on `/api/app/*`); MCP:
|
|
62
|
+
the URL (`/mcp`, `?target=draft`, `?scope=assistant`); tools and the
|
|
63
|
+
assistant pass a `CallScope`. Default live. `getHost()` is live only; the
|
|
64
|
+
draft host is reached only via `hostFor('draft')`; `invariants.test.ts` keeps
|
|
65
|
+
request paths on `hostFor` and `autoApprove: true` in `openDraftHost`. While a
|
|
66
|
+
draft is open live is frozen (also after a restart): builder calls naming live
|
|
67
|
+
are refused naming the draft; live records keep working. `publishDraft` is
|
|
68
|
+
always an Approval (`publish-draft`, pinned to the preview digest; the engine
|
|
69
|
+
refuses at apply if the chain changed); a discard is direct from the UI route,
|
|
70
|
+
an Approval from a tool. Approvals only ever run against live. Table changes
|
|
71
|
+
here are new tables only (`prepareShellSchema` cannot ALTER).
|
|
52
72
|
- **Decorator configs are literals** (`@smrt({...})` on `App`, `Approval`). The
|
|
53
73
|
scanner reads them statically; a shared constant silently falls back to full
|
|
54
74
|
CRUD routes. Collections are undecorated. The shell models expose only their
|
|
@@ -62,7 +82,9 @@ SvelteKit or `src/lib`). README has the route and JSON contracts.
|
|
|
62
82
|
`<data root>/user-app/` (`app.db`, `snapshots/`, archives inside app.db):
|
|
63
83
|
plain files and directories inside the app-runtime data root, no symlinks, so
|
|
64
84
|
the encrypted backup seals them. `PORT` and `SMRT_DATA_DIR` are honored (the
|
|
65
|
-
desktop sidecar contract); health is `/api/_runtime/health
|
|
85
|
+
desktop sidecar contract); health is `/api/_runtime/health` and the desktop
|
|
86
|
+
wrapper's owner link is `/api/_runtime/owner-link` (both app-runtime handlers;
|
|
87
|
+
the Tricia desktop product is `smrt-desktop/examples/tricia`).
|
|
66
88
|
- **App-subclass rule** lives in the engine (`enableRecipe` creates app
|
|
67
89
|
subclasses of shipped models). The shell never registers user models in its
|
|
68
90
|
own registry.
|
|
@@ -84,14 +106,34 @@ SvelteKit or `src/lib`). README has the route and JSON contracts.
|
|
|
84
106
|
decides approvals (`approval_decide` is not in the catalog, and
|
|
85
107
|
`ASSISTANT_DENIED_TOOLS` repeats it): only the signed-in owner does, in the UI. A risky call is held (`holdForUser`) on its Approval; only the
|
|
86
108
|
owner's `Approval.decide` applies it. The loop is bounded (`DEFAULT_LIMITS`),
|
|
87
|
-
serial, aborts on disconnect.
|
|
88
|
-
`claude-cli
|
|
109
|
+
serial, aborts on disconnect. Three engines (`assistant` preference): `api`
|
|
110
|
+
(`config.ts` refuses `claude-cli` and any configured `chatgpt-plan`; no
|
|
111
|
+
in-app Claude subscription login), `chatgpt` (the owner's Sign in with
|
|
112
|
+
ChatGPT, a PREVIEW for open source apps: `chatgpt.ts`, `assistant/model.ts`,
|
|
113
|
+
README "Sign in with ChatGPT"; runs only on the target smrt-inference's
|
|
114
|
+
sign-in resolver made for the `AppShellAssistantChatGPT` request, never an
|
|
115
|
+
API key; `chat()` + `onProgress`, none of `maxTokens`/sampling/`user`/
|
|
116
|
+
response format/reasoning/forced tool choice; the SDK keeps the credentials
|
|
117
|
+
in `<data root>/chatgpt/chatgpt.json`, never read, logged or returned here)
|
|
118
|
+
and a local agent CLI (`claude-code`/`codex`, the person's own signed-in
|
|
119
|
+
tool; `agent-cli/`, README "The local agent CLI engine"). The CLI engine gets
|
|
120
|
+
tools only through `/mcp` under a short-lived owner-bound `assistant.use`
|
|
121
|
+
token in the child env (never argv, file or log), with built-ins absent (Claude) or OS-confined (Codex; inference
|
|
122
|
+
`agents/engines.md`), provider keys stripped and the stream guarded; never read a
|
|
123
|
+
CLI's credential files or keychain, never offer or proxy its login (it opens
|
|
124
|
+
the vendor's page itself), plain-text product names only. Approvals stay the
|
|
125
|
+
shell's: `invariants.test.ts` keeps `agent-cli/` and `agent-turn.ts` free of
|
|
126
|
+
`decideApproval` / `approved: true`. The API key is resolved by
|
|
127
|
+
`resolveInferenceTarget` through the resolver `installShellInference`
|
|
128
|
+
registered: the owner's key saved in Settings, then stored bindings, then
|
|
129
|
+
config/env as `base`. Keys are written only by the owner-only `saveKey`
|
|
130
|
+
action; none is logged or returned (`inference-settings.test.ts`). Model errors are never
|
|
89
131
|
forwarded or logged beyond class/code (SDK messages can echo keys). Tests use
|
|
90
132
|
a stub through `modelFactory`.
|
|
91
133
|
|
|
92
134
|
## Tests and build
|
|
93
135
|
|
|
94
|
-
`vitest run` (in-process, SQLite, temp dirs; `test-support.ts` seeds an owner
|
|
136
|
+
`vitest run` then `vitest run -c vitest.dom.config.ts` (the layout's jsdom component tests, `*.dom.test.ts`, over smrt-svelte's built `dist`; rebuild smrt-svelte first). Node suite: in-process, SQLite, temp dirs; `test-support.ts` seeds an owner
|
|
95
137
|
and a member; it opens the real driver because the vitest setup would
|
|
96
138
|
pre-create tables). `pnpm build` (tsc engine, `svelte-kit sync`, `vite build`;
|
|
97
139
|
the smrt() plugin generates `src/routes/api/{apps,approvals,_changes,_events,
|
package/README.md
CHANGED
|
@@ -12,6 +12,32 @@ smrt-app-shell (SvelteKit + adapter-node, app-runtime local profile)
|
|
|
12
12
|
└── <data root>/user-app/ app.db, snapshots (plain files; the newest 20 `method-*` are kept, clean up prunes old migration ones)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Shared assistant pieces (Tricia)
|
|
16
|
+
|
|
17
|
+
The saved data-shell product keeps its own assistant panel, routes, thread
|
|
18
|
+
history, builder and draft policy. Engines use smrt-inference; human tool
|
|
19
|
+
labels use smrt-chat; approval decisions/consumption use app-runtime's
|
|
20
|
+
`OwnerApprovalAuthority` and chat's observation-only hold bridge; connection
|
|
21
|
+
onboarding/settings use smrt-svelte's `ConnectAi` and `mountConnectAiRoutes`.
|
|
22
|
+
|
|
23
|
+
Existing approvals **stay in `approvals`**: no data copy, migration or new
|
|
24
|
+
approval ids. `Approval` is a compatibility model/endpoint facade and
|
|
25
|
+
`approval-store.ts` adapts those rows to the shared authority. Pending requests
|
|
26
|
+
retain their original indefinite lifetime, preview and requester. Decided
|
|
27
|
+
history retains its timestamps/results. Atomic conditional updates decide and
|
|
28
|
+
consume once across authorities; execution is recorded before live effects.
|
|
29
|
+
Interrupted approvals never replay automatically. An old approved row without a
|
|
30
|
+
result remains an unknown execution outcome and needs manual reconciliation.
|
|
31
|
+
The adapter refuses storage with more than one installation tenant because the
|
|
32
|
+
old rows do not encode tenancy; it cannot safely infer another ownership model.
|
|
33
|
+
|
|
34
|
+
Opt-in shared compatibility options preserve generic record chips, compact
|
|
35
|
+
connection wording/grouping, the settings-only ChatGPT model preference, API
|
|
36
|
+
selection without a configured key, and remote sign-out confirmation. The
|
|
37
|
+
shell keeps its existing sign-out fallback to the API preference, Fresh/Skip
|
|
38
|
+
steps, detailed builder/code previews and draft-review link. Other applications
|
|
39
|
+
keep the shared defaults. See [adapter contracts and validation](agents/shared-pieces.md).
|
|
40
|
+
|
|
15
41
|
## Run it
|
|
16
42
|
|
|
17
43
|
Build once, then set up the data folder and the owner link, then start the
|
|
@@ -34,6 +60,17 @@ the app's state folder, not the data folder). If the link is lost, stop the
|
|
|
34
60
|
server and run `smrt app recover` for a new one. Setup is loopback only and
|
|
35
61
|
single use. Health: `GET /api/_runtime/health`.
|
|
36
62
|
|
|
63
|
+
Desktop: the shell also mounts `GET /api/_runtime/owner-link`
|
|
64
|
+
(`createOwnerLinkHandler`), the route the desktop wrapper opens its window on.
|
|
65
|
+
It is loopback-only and answers 403 without the wrapper's per-launch secret, so
|
|
66
|
+
it does nothing outside the wrapper. With that secret it uses `autoClaim`: the
|
|
67
|
+
first launch creates the local owner (named after the OS account, no email) and
|
|
68
|
+
signs them in, a later launch with a lost session cookie signs the sole owner
|
|
69
|
+
back in, and the window lands on `/`. No owner form is shown. Under the wrapper
|
|
70
|
+
the signed-out and `/setup` pages say to quit and reopen the app instead of
|
|
71
|
+
naming terminal commands. Tricia, the packaged desktop product, is
|
|
72
|
+
[`smrt-desktop/examples/tricia`](../smrt-desktop/examples/tricia/README.md).
|
|
73
|
+
|
|
37
74
|
## Builder operations (MCP tools and `POST /api/apps/<operation>`)
|
|
38
75
|
|
|
39
76
|
The same names and parameters on both surfaces. A wrong-shaped argument is a
|
|
@@ -52,6 +89,8 @@ The same names and parameters on both surfaces. A wrong-shaped argument is a
|
|
|
52
89
|
| `enableRecipe` | `recipe` |
|
|
53
90
|
| `cleanUp` | `keep?` |
|
|
54
91
|
| `importTemplate`, `exportTemplate` | `data`, `includeRecords?` |
|
|
92
|
+
| `undoTo` (`app_undoto`) | `version` |
|
|
93
|
+
| `startDraft`, `previewPublish`, `publishDraft`, `discardDraft` | `name?`; none; `expect?`; none. See [Draft then publish](#draft-then-publish-the-shell). `?target=live\|draft` (REST) names the copy a builder operation acts on |
|
|
55
94
|
|
|
56
95
|
A FieldDef is `{ type, label?, required?, choices?, ref? }`; `type` is one of
|
|
57
96
|
text, number, integer, boolean, date, money, choice, reference, json. Field names
|
|
@@ -65,16 +104,16 @@ owner). Writes must be same-origin `application/json`. Errors are
|
|
|
65
104
|
|
|
66
105
|
| Route | Body | Response |
|
|
67
106
|
|---|---|---|
|
|
68
|
-
| `GET /api/shell/describe
|
|
69
|
-
| `GET /api/shell/history
|
|
70
|
-
| `POST /api/shell/undo
|
|
107
|
+
| `GET /api/shell/describe?target=` | | `{ mode: 'live'\|'draft', draft: DraftInfo \| null, frozen, name, version, recipes, models: { [Model]: { kind: 'app'\|'shipped-subclass', label, table, className, extends, fields: { [name]: FieldDef }, ownFields: string[], methods: { [name]: { description, params } } } }, pendingApprovals }` (see [Draft then publish](#draft-then-publish-the-shell)) |
|
|
108
|
+
| `GET /api/shell/history?target=` | | `{ mode, versions: { version, parent, author, summary, createdAt, purged, current, publish: { draftId, name } \| null }[] }` oldest first |
|
|
109
|
+
| `POST /api/shell/undo?target=` | none | change result `{ version, summary, statements, appRestartMs, ... }`; 409 if purged; **202** `PendingApproval` (with `preview.replaced`, the rows that would be overwritten) when the change being undone converted a field's type |
|
|
71
110
|
| `GET /api/shell/approvals?status=` | | `{ approvals: ApprovalView[], pending }`; `status` is `pending`, `approved`, `denied` or `failed` |
|
|
72
111
|
| `POST /api/shell/approvals/<id>` | `{ approve: boolean }` | `{ id, status: 'approved'\|'denied'\|'failed', result?, error? }`; 404 unknown, 409 already decided |
|
|
73
|
-
| `GET /api/shell/records/<Model>?limit=` | | `{ records: object[] }` |
|
|
74
|
-
| `POST /api/shell/records/<Model
|
|
75
|
-
| `GET /PUT /api/shell/records/<Model>/<id
|
|
76
|
-
| `DELETE /api/shell/records/<Model>/<id
|
|
77
|
-
| `/api/app/<route>` (any method) | as the app's REST | the app's generated REST (`/api/app/chores` is the app's `/api/chores`); `DELETE` answers **202** `PendingApproval`; only the app's own collection routes are served (`sync/apply`, `_changes`, `_events` and unknown collections are 404) |
|
|
112
|
+
| `GET /api/shell/records/<Model>?limit=&target=` | | `{ records: object[] }` |
|
|
113
|
+
| `POST /api/shell/records/<Model>?target=` | the record's fields | 201 the record |
|
|
114
|
+
| `GET /PUT /api/shell/records/<Model>/<id>?target=` | PUT: changed fields | the record |
|
|
115
|
+
| `DELETE /api/shell/records/<Model>/<id>?target=` | | live: **202** `PendingApproval`, nothing is deleted until approved; draft: **200** `{ deleted: id }` |
|
|
116
|
+
| `/api/app/<route>` (any method; copy in the `x-shell-target` header) | as the app's REST | the app's generated REST (`/api/app/chores` is the app's `/api/chores`); `DELETE` answers **202** `PendingApproval`; only the app's own collection routes are served (`sync/apply`, `_changes`, `_events` and unknown collections are 404) |
|
|
78
117
|
| `GET /api/events` | | server-sent events |
|
|
79
118
|
| `POST /mcp` | MCP Streamable HTTP | shell tools plus app tools, one policy |
|
|
80
119
|
|
|
@@ -83,8 +122,8 @@ owner). Writes must be same-origin `application/json`. Errors are
|
|
|
83
122
|
where `action` is `{ kind: 'command', command } | { kind: 'tool', name, args } | { kind: 'rest', method, url } | { kind: 'record-delete', model, id }`.
|
|
84
123
|
|
|
85
124
|
`GET /api/events` sends `event: ready`, then `event: shell` with JSON
|
|
86
|
-
`{ type: 'changed', version, summary }` (definition changed: reload
|
|
87
|
-
`describe`, the menu and the tool catalog), `{ type: 'records', model }`
|
|
125
|
+
`{ type: 'changed', target, version, summary }` (definition changed: reload
|
|
126
|
+
`describe`, the menu and the tool catalog), `{ type: 'records', target, model }`
|
|
88
127
|
(`model` may be null: reload lists) and `{ type: 'approval', id, status, summary }`
|
|
89
128
|
(`pending`, `approved`, `denied`, `failed`). A `: ping` comment every 25 s.
|
|
90
129
|
|
|
@@ -93,7 +132,8 @@ REST (`POST /api/apps/<op>`) and MCP tools (`app_definemodel`, `app_addfield`,
|
|
|
93
132
|
`app_hidefield`, `app_renamefield`, `app_removefield`, `app_changefieldtype`,
|
|
94
133
|
`app_removemodel`, `app_addmethod`, `app_enablerecipe`, `app_undo`,
|
|
95
134
|
`app_cleanup`, `app_exporttemplate`, `app_importtemplate`, `app_describe`,
|
|
96
|
-
`app_history`
|
|
135
|
+
`app_history`, `app_undoto`, `app_startdraft`, `app_previewpublish`,
|
|
136
|
+
`app_publishdraft`, `app_discarddraft`), plus `approval_pending`. There is no tool to decide an approval: only the
|
|
97
137
|
owner, signed in, decides in the UI, so no model-driven client (the assistant
|
|
98
138
|
or an outside MCP client) can approve its own call. The risky ones
|
|
99
139
|
(`addMethod`, `removeField`, `removeModel`, `changeFieldType`, `cleanUp`, an
|
|
@@ -105,18 +145,45 @@ or an outside MCP client) can approve its own call. The risky ones
|
|
|
105
145
|
|
|
106
146
|
- `toolsFor(principal): Promise<CatalogTool[]>`: the merged catalog (`{ name, description, inputSchema, annotations?, side: 'shell'|'app', risky }`); empty unless `principal` is the owner.
|
|
107
147
|
- `authorizeCall(principal, name): Promise<CatalogTool>`: throws `PolicyError` (`status` 401 no principal, 403 not the owner or unknown tool).
|
|
108
|
-
- `callTool(principal, name, args)`: authorize and run. A risky call returns a `PendingApproval`.
|
|
148
|
+
- `callTool(principal, name, args, scope?)`: authorize and run on the copies `scope` names (default: live). A risky call returns a `PendingApproval`.
|
|
109
149
|
- `Principal = { id, tenantId, scopes? }`; get one from the request with `principalFromLocals(locals)` (`owner.ts`).
|
|
110
150
|
|
|
151
|
+
## Settings
|
|
152
|
+
|
|
153
|
+
`/settings` is the section menu (`ShellSettingsPage`): Appearance, Account,
|
|
154
|
+
Layout and Shortcuts, plus **Assistant** (`/settings/assistant`: the engine,
|
|
155
|
+
see Onboarding) and **Inference** (`/settings/inference`), the
|
|
156
|
+
`InferenceSettingsPanel` from smrt-inference. Owner only: every load and action
|
|
157
|
+
runs as the verified owner (a member or a stranger gets 403 before any body is
|
|
158
|
+
read). Saving a provider key (`saveKey`) binds it to its endpoint, stores it in
|
|
159
|
+
smrt-secrets (sealed under the app-runtime master key, created once into the
|
|
160
|
+
vault key store; `SMRT_SECRET_MASTER_KEY` wins), and `keySet` is read back from
|
|
161
|
+
the resolver's redacted view; the key itself never returns to the browser. The
|
|
162
|
+
shell registers smrt-inference's user resolver at startup, which is what the
|
|
163
|
+
assistant resolves through. The inference, preferences and secrets tables are
|
|
164
|
+
created by `prepareShellSchema` on first start (`consumer.packages` in
|
|
165
|
+
`smrt.config.ts` lists their packages). The load and writes are smrt-inference's
|
|
166
|
+
`createInferenceSettingsController`, shared with the template.
|
|
167
|
+
|
|
111
168
|
## The assistant
|
|
112
169
|
|
|
113
|
-
The
|
|
114
|
-
|
|
170
|
+
The chat column (`src/lib/assistant/AssistantPanel.svelte`, the `panel` of
|
|
171
|
+
smrt-svelte's `AppShell` in `src/routes/+layout.svelte`, mounted for the owner)
|
|
172
|
+
talks to `mountAssistantRoutes` (`src/lib/server/assistant/`). The layout is
|
|
173
|
+
left sidebar, chat, content; the voice and assistant toggles and the user menu
|
|
174
|
+
sit in the sidebar footer. The sidebar collapses to an icon rail (nav icons with
|
|
175
|
+
tooltips, the footer controls as icons) from the chevron above the footer; the
|
|
176
|
+
choice is remembered per owner, and the footer has no divider line (it would sit
|
|
177
|
+
beside the chat composer's top border). The boundary between chat and content is a draggable
|
|
178
|
+
separator (arrow keys, double-click resets); its width and the open state are
|
|
179
|
+
remembered per owner. As a plugin view (iframe, or `?embed=plugin`) there is no
|
|
180
|
+
chat and no toggles: the host app is the chat. Voice mode shows as not
|
|
181
|
+
available yet; nothing connects speech to the assistant.
|
|
115
182
|
Owner session required on all of them (401/403); the POST is same-origin.
|
|
116
183
|
|
|
117
184
|
| Route | Body | Response |
|
|
118
185
|
|---|---|---|
|
|
119
|
-
| `GET /api/assistant/status` | | `{ configured: boolean, code?, message? }`; never a key, provider or
|
|
186
|
+
| `GET /api/assistant/status` | | `{ configured: boolean, engine?, engineLabel?, code?, message?, action? }`; never a key, provider, endpoint or path. `engine` is `api`, `claude-code`, `codex` or `chatgpt`. `code` is `assistant_not_configured` (no API key), `assistant_provider_refused` (subscription provider configured), `ai_unavailable` (inference off), `agent_cli_missing` or `agent_cli_signed_out` (a local CLI engine that is not ready), `chatgpt_signed_out` (Sign in with ChatGPT not done); `action` is the link that fixes it |
|
|
120
187
|
| `GET /api/assistant/threads` | | `{ threads: { id, title, updatedAt }[] }` newest first |
|
|
121
188
|
| `GET /api/assistant/threads/<id>` | | `{ thread, messages: ThreadMessage[] }`; 404 for a missing or foreign thread |
|
|
122
189
|
| `POST /api/assistant/turn` | `{ message, threadId? }` | `text/event-stream`, `event: turn`, JSON `TurnEvent` (below). **503** `{ type: 'unavailable', configured: false, code, error }` when the assistant cannot run; 409 while another turn is running |
|
|
@@ -124,7 +191,7 @@ Owner session required on all of them (401/403); the POST is same-origin.
|
|
|
124
191
|
`TurnEvent` (`src/lib/assistant/events.ts`): `thread {threadId}`, `text {delta}`,
|
|
125
192
|
`tool_call {id,name,args}`, `tool_result {id,name,ok,summary}`,
|
|
126
193
|
`approval_required {id,name,approvalId,summary,preview}`,
|
|
127
|
-
`approval_decided {id,approvalId,status}`, `error {code,message}`,
|
|
194
|
+
`approval_decided {id,approvalId,status}`, `error {code,message,action?}`,
|
|
128
195
|
`done {stop}` (`complete | max_steps | max_tokens | timeout | aborted | waiting | error`).
|
|
129
196
|
Closing the request (the dock's Stop button, a closed tab) aborts the model
|
|
130
197
|
call, the running tool and any wait.
|
|
@@ -140,13 +207,28 @@ call, the running tool and any wait.
|
|
|
140
207
|
wait times out or the client leaves, the model is told it is waiting, the
|
|
141
208
|
turn ends as `waiting`/`aborted`, and the Approval stays pending, unapplied.
|
|
142
209
|
The assistant has no way to set the engine's `approved` flag.
|
|
143
|
-
- **
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
210
|
+
- **Engines.** Three, chosen by the owner (the non-secret `assistant`
|
|
211
|
+
preference, `engine` = `api` | `claude-code` | `codex` | `chatgpt`; default
|
|
212
|
+
`api`):
|
|
213
|
+
- **API key** (`api`): `@happyvertical/ai` through core's inference tiers
|
|
214
|
+
(`resolveInferenceTarget`, tier `good`, override
|
|
215
|
+
`SMRT_APP_SHELL_ASSISTANT_TIER`). The key is the owner's, pasted once in
|
|
216
|
+
**Settings > Inference** (`/settings/inference`, see below), or the
|
|
217
|
+
deployment's: the `ai` block of `smrt.config.ts`, `SMRT_AI_*` or the
|
|
218
|
+
provider's own variable (`ANTHROPIC_API_KEY`). The saved key wins, then the
|
|
219
|
+
stored tier bindings, then config/env, so a desktop app launched from
|
|
220
|
+
Finder works with no environment. The key is read only server side, never
|
|
221
|
+
logged or sent to the browser. Subscription providers (`claude-cli`,
|
|
222
|
+
`chatgpt-plan` from configuration) are refused here, as is any in-app
|
|
223
|
+
Claude subscription login.
|
|
224
|
+
- **Local agent CLI** (`claude-code`, `codex`): the turn runs through the
|
|
225
|
+
person's own, already installed and signed-in Claude Code or Codex
|
|
226
|
+
(`src/lib/server/agent-cli/`, `assistant/agent-turn.ts`). See below.
|
|
227
|
+
- **ChatGPT** (`chatgpt`): the owner's own ChatGPT plan through OpenAI's
|
|
228
|
+
official Sign in with ChatGPT flow (see below).
|
|
229
|
+
With no usable engine, status/turn answer the typed not-configured state, the
|
|
230
|
+
dock shows it with a link to fix it, and it checks again each time it is
|
|
231
|
+
opened, so a saved key or a finished sign-in needs no restart.
|
|
150
232
|
- **Bounds** (`DEFAULT_LIMITS` in `turn.ts`): 8 model rounds, 2048 output
|
|
151
233
|
tokens per call, 60 000 tokens per turn, 4 calls per round, 60 s per model
|
|
152
234
|
call, 180 s of model/tool time per turn, 15 min per approval wait.
|
|
@@ -154,6 +236,261 @@ call, the running tool and any wait.
|
|
|
154
236
|
- **Threads** persist in shell.db (`assistant_threads`, `assistant_messages`).
|
|
155
237
|
- **Tests** inject a stub model (`modelFactory` option, `test-helpers.ts`).
|
|
156
238
|
|
|
239
|
+
The reusable CLI engine, ChatGPT model adapter and connection diagnostics live in
|
|
240
|
+
[smrt-inference/server](../inference/agents/engines.md). The shell supplies its
|
|
241
|
+
owner token scope, MCP route, thread persistence, draft guidance and approval
|
|
242
|
+
presentation through thin host adapters; its policy remains the authority.
|
|
243
|
+
|
|
244
|
+
### Sign in with ChatGPT (a preview for open source apps)
|
|
245
|
+
|
|
246
|
+
OpenAI's official flow for local open source apps, a **preview**: the
|
|
247
|
+
authorization basis and its limits are in OpenAI's
|
|
248
|
+
[preview limitations](https://developers.openai.com/siwc/token-sharing-open-source/preview-limitations).
|
|
249
|
+
Claude has no equivalent here: its subscription stays on the Claude Code
|
|
250
|
+
engine below, and `claude-cli` and any in-app Claude subscription login stay
|
|
251
|
+
refused.
|
|
252
|
+
|
|
253
|
+
- **Sign-in** (`lib/server/chatgpt.ts`, smrt-inference's
|
|
254
|
+
`createChatGPTPlanConnection`): "Sign in with ChatGPT" on `/welcome` and in
|
|
255
|
+
Settings > Assistant. Under the desktop wrapper this process opens the system
|
|
256
|
+
browser (`open` / `xdg-open` / `cmd /c start`) after checking the address is
|
|
257
|
+
https on `auth.openai.com`; in a plain browser session the page shows a normal
|
|
258
|
+
link. The SDK's loopback listener (127.0.0.1, PKCE, dynamic client
|
|
259
|
+
registration, OIDC validation) receives the callback, the page polls until it
|
|
260
|
+
is signed in and then selects the engine.
|
|
261
|
+
- **Credentials** live only in the SDK's own owner-only file,
|
|
262
|
+
`<data root>/chatgpt/chatgpt.json` (directory `0700`, file `0600`, outside any
|
|
263
|
+
source tree; the encrypted backup that seals the data root seals it too).
|
|
264
|
+
This app never reads it and never logs, returns or copies a token: the
|
|
265
|
+
provider gets a `{ accessToken(clientId) }` handle, the browser sees
|
|
266
|
+
signed in / signing in / an account label. Sign out clears it and asks OpenAI
|
|
267
|
+
to revoke. `SMRT_APP_SHELL_NAME` sets the app name OpenAI's page shows.
|
|
268
|
+
- **The turn** (`assistant/model.ts`): the `chatgpt` engine asks the resolver
|
|
269
|
+
smrt-inference composed (`installShellInference`) with its own request name,
|
|
270
|
+
and runs ONLY on the `chatgpt-plan` target that sign-in produced; with nobody
|
|
271
|
+
signed in it is unavailable ("Sign in with ChatGPT"), never an API key, never
|
|
272
|
+
another provider. The loop uses `chat()` with `onProgress` (the provider's
|
|
273
|
+
`stream()` rejects tools) and a tool namespace (`shell_tools`); it sends no
|
|
274
|
+
`maxTokens`, sampling, `user`, response format, reasoning option or forced
|
|
275
|
+
tool choice, which the preview refuses. The model is the owner's pick in
|
|
276
|
+
Settings > Assistant, else the first model in the account's catalog.
|
|
277
|
+
Approvals, the offer gate and the deny list are unchanged.
|
|
278
|
+
- **Failures** become dock messages: usage limit, revoked session and workspace
|
|
279
|
+
refusals (the last two with "Sign in again", linking to
|
|
280
|
+
Settings > Assistant), "unavailable, still signed in", and the preview's
|
|
281
|
+
unsupported-capability refusal. Only the error class and code are logged.
|
|
282
|
+
- **Tests** stub the provider (`assistant/chatgpt-engine.test.ts`), the
|
|
283
|
+
connection (`routes/onboarding-chatgpt.test.ts`) and the opener
|
|
284
|
+
(`lib/server/chatgpt.test.ts`); nothing signs in.
|
|
285
|
+
|
|
286
|
+
### The local agent CLI engine
|
|
287
|
+
|
|
288
|
+
**Rule (two lines).** No in-app Claude subscription login: this app never
|
|
289
|
+
offers Claude.ai sign-in and never collects, stores or passes on those
|
|
290
|
+
credentials. The local-CLI engine only runs the person's own, unmodified Claude
|
|
291
|
+
Code or Codex, signed in through the vendor's own flow and billed to them
|
|
292
|
+
([Claude Code legal and compliance](https://code.claude.com/docs/en/legal-and-compliance)).
|
|
293
|
+
Product names appear as plain text, no logos.
|
|
294
|
+
|
|
295
|
+
- **Detection** (`agent-cli/locate.ts`, `detect.ts`): the executable on PATH,
|
|
296
|
+
then the usual install places (`~/.local/bin`, `~/.claude/local`, Homebrew,
|
|
297
|
+
`/usr/local/bin`, npm/pnpm/volta/bun/nvm bins, Windows equivalents), then the
|
|
298
|
+
person's login shell (`command -v`), so an app started from Finder finds it.
|
|
299
|
+
Version and sign-in come only from the tool's own commands (`--version`,
|
|
300
|
+
`claude auth status`, `codex login status`); no credential file or keychain
|
|
301
|
+
item is read, and the browser sees only installed / version / signed in. Not
|
|
302
|
+
installed: a link to the vendor's install page, nothing is installed for them.
|
|
303
|
+
Windows runs a real `.exe` (a `.cmd` shim is not started, there is no shell).
|
|
304
|
+
- **Connect** (`agent-cli/login.ts`): signed out, it starts the tool's own
|
|
305
|
+
sign-in (`claude auth login`, `codex login`), which opens the vendor's page
|
|
306
|
+
in the system browser; the app only watches the process end, then asks the
|
|
307
|
+
status command again.
|
|
308
|
+
- **A turn** (`agent-cli/session.ts`, `assistant/agent-turn.ts`):
|
|
309
|
+
`claude -p --output-format stream-json --verbose --include-partial-messages`
|
|
310
|
+
or `codex exec --json`, the prompt on stdin, streamed JSON mapped onto the
|
|
311
|
+
same `TurnEvent`s (the dock has no engine-specific code). Locked down:
|
|
312
|
+
- Tools: only the shell's, from `/mcp`, with the same owner policy and
|
|
313
|
+
Approval gate. Every built-in tool that touches the machine is off (Claude:
|
|
314
|
+
`--tools ""` plus a strict MCP config; Codex: shell, exec, apps, plugins,
|
|
315
|
+
hooks, sub-agents, code mode and more disabled, read-only sandbox, approvals
|
|
316
|
+
never, no web search). The stream is also checked: any other tool, or an
|
|
317
|
+
unknown Codex item kind, stops the turn.
|
|
318
|
+
- Token: a short-lived (15 min), owner-bound, `assistant.use`-scoped
|
|
319
|
+
app-runtime local MCP token, issued under one holder (each turn rotates the
|
|
320
|
+
last) and revoked when the turn ends. It is in the child's environment only:
|
|
321
|
+
never argv, a file or a log, and only ever sent to a loopback URL. Claude
|
|
322
|
+
reads it through `${VAR}` in its MCP header config; Codex through
|
|
323
|
+
`bearer_token_env_var`.
|
|
324
|
+
- Environment: an allow-list (`agent-cli/env.ts`); `ANTHROPIC_*`, `OPENAI_*`,
|
|
325
|
+
`CODEX_*`, `CLAUDE_CODE_*` and `SMRT_*` never reach the child, so the tool
|
|
326
|
+
uses the person's own login. Working directory: an empty private folder
|
|
327
|
+
`<data root>/agent-cli/work`. Claude loads none of the person's settings,
|
|
328
|
+
hooks, skills or plugins; Codex ignores their `config.toml` and rules.
|
|
329
|
+
- Approvals: a risky call returns `pending_approval` from `/mcp`; the turn
|
|
330
|
+
shows the card (`approval_required`) and ends `waiting`. Only the owner's
|
|
331
|
+
decision applies it (`Approval.decide`). The tool is told not to retry.
|
|
332
|
+
- Bounds: 10 minutes per turn; cancel (the Stop button) and timeout kill the
|
|
333
|
+
whole process tree. A later turn resumes the tool's own session (stored on
|
|
334
|
+
the thread's last assistant message), else starts a new one.
|
|
335
|
+
- Known gaps: Codex still offers its own code-mode `exec` and `apply_patch`
|
|
336
|
+
tools to the model (no flag removes them; the read-only sandbox and the
|
|
337
|
+
stream guard are what stop them), and it still reads the person's own global
|
|
338
|
+
Codex instructions file (its home folder AGENTS.md). Codex 0.162.1 needs `--enable mcp_2026_07_28` to
|
|
339
|
+
reach `/mcp`. The tool keeps its own session transcript in its own folder.
|
|
340
|
+
- **Tests:** stub CLIs (shared `smrt-inference/test-support`) over the real
|
|
341
|
+
`/mcp` route (`assistant/agent-turn.test.ts`); fake binaries on a temp PATH
|
|
342
|
+
(`agent-cli/detect.test.ts`). An opt-in run against a real installed CLI:
|
|
343
|
+
`REAL_AGENT_CLI=claude-code|codex npx vitest run
|
|
344
|
+
src/lib/server/assistant/agent-cli.manual.test.ts` (spends the person's own
|
|
345
|
+
subscription, one short turn).
|
|
346
|
+
|
|
347
|
+
## Onboarding
|
|
348
|
+
|
|
349
|
+
`/welcome` (owner only; an owner with an empty app who has not finished or
|
|
350
|
+
skipped it is sent there once): **1. Connect your AI** (Use Claude Code,
|
|
351
|
+
Sign in with ChatGPT, plus Advanced: the full inference setup at
|
|
352
|
+
`/settings/inference`, which links back; Codex is an advanced option under
|
|
353
|
+
Settings > Assistant, not on this page) and **2. How do you want to start?** (Fresh: an empty app with the
|
|
354
|
+
assistant dock open; Wizard: shown, disabled, coming soon). Skippable. Finished
|
|
355
|
+
or skipped is stored as `onboarding` in the `assistant` preference
|
|
356
|
+
(`agent-cli/settings.ts`, the smrt-preferences store smrt-inference also uses;
|
|
357
|
+
not a member of the `inference` payload, which the settings panel replaces whole,
|
|
358
|
+
and not a tier target of core's inference model). **Settings > Assistant**
|
|
359
|
+
(`/settings/assistant`) switches engine and re-opens the steps. JSON routes
|
|
360
|
+
(owner only, same-origin writes): `GET /api/shell/onboarding[?fresh=1]`,
|
|
361
|
+
`POST /api/shell/onboarding/{connect,engine,complete,skip}`, and
|
|
362
|
+
`GET`/`POST /api/shell/onboarding/chatgpt` (start, sign-out, model;
|
|
363
|
+
`lib/server/onboarding.ts`).
|
|
364
|
+
|
|
365
|
+
## Draft then publish (the shell)
|
|
366
|
+
|
|
367
|
+
A draft is a test copy of the person's app (its definition and its records at
|
|
368
|
+
that moment) that they build in while the live app keeps working. Rules the UI
|
|
369
|
+
can rely on:
|
|
370
|
+
|
|
371
|
+
- One draft at a time. While it is open **live structure is frozen** (frozen
|
|
372
|
+
again after every restart); live **records** keep working (create, edit,
|
|
373
|
+
delete, reports, the app's own REST and tools).
|
|
374
|
+
- A draft's own records are throw-away test data and are **not carried** at
|
|
375
|
+
publish; records added to live meanwhile are kept.
|
|
376
|
+
- A publish lands as N ordinary live versions (summaries tagged
|
|
377
|
+
`[draft: <name>] ...`) and can be undone in one step with `undoTo`.
|
|
378
|
+
- **Every request names its copy**, there is no "current host": REST uses
|
|
379
|
+
`?target=live|draft` (absent = `live`; anything else is 400), `/api/app/*`
|
|
380
|
+
uses the `x-shell-target` header (its query string belongs to the app), MCP
|
|
381
|
+
uses the URL. `target=draft` with no draft open is 404 `no draft open`.
|
|
382
|
+
- A structural request that names `live` while a draft is open is **409**
|
|
383
|
+
`The draft "<name>" is open, so the live app's structure is frozen. Make this
|
|
384
|
+
change in the draft (target=draft), or publish or discard the draft first.`
|
|
385
|
+
- A risky change in the draft (remove a field or model, change a type, add a
|
|
386
|
+
method, delete a record, a DELETE through the app's REST) applies at once: the
|
|
387
|
+
draft host approves its own. On live it is an Approval, as before.
|
|
388
|
+
|
|
389
|
+
### Routes (owner only: 401 signed out, 403 anyone else; writes same-origin JSON)
|
|
390
|
+
|
|
391
|
+
| Route | Body | Response |
|
|
392
|
+
|---|---|---|
|
|
393
|
+
| `GET /api/shell/draft` | | `{ draft: DraftInfo \| null, view: 'live'\|'draft' }` |
|
|
394
|
+
| `POST /api/shell/draft` | `{ name? }` | **201** `DraftInfo`; 409 if one is already open |
|
|
395
|
+
| `POST /api/shell/draft/preview` | none | `{ draft: DraftInfo, preview: PublishPreview }`; 404 no draft; 409 unpublishable |
|
|
396
|
+
| `POST /api/shell/draft/publish` | `{ expect? }` (the preview's `digest`) | **always 202** `PendingApproval`; 409 when `expect` no longer matches (review again) |
|
|
397
|
+
| `POST /api/shell/draft/discard` | `{ draftId? }` | `{ discarded: name }`; the owner's direct action (the UI confirms first); 404 none, 409 wrong id |
|
|
398
|
+
| `GET /api/shell/draft/view` | | `{ view }` (`live` whenever no draft is open) |
|
|
399
|
+
| `PUT /api/shell/draft/view` | `{ view: 'live'\|'draft' }` | `{ view }`; `draft` with no usable draft is 409 |
|
|
400
|
+
| `POST /api/shell/undo-to?target=` | `{ version }` | the change result; 202 `PendingApproval` when an undone change converted a field's type; 409 purged |
|
|
401
|
+
| `POST /api/apps/{startDraft,previewPublish,publishDraft,discardDraft,undoTo}` | as the tools | the same operations as generated REST; `publishDraft` and `discardDraft` answer **202** (an Approval) |
|
|
402
|
+
|
|
403
|
+
`DraftInfo = { id, name, status: 'open', baseVersion, version, changes,
|
|
404
|
+
createdAt, createdBy, liveVersion, liveFrozen, publishable, problem, recovery }`.
|
|
405
|
+
`changes` counts the changes made in the draft; `version` is null when its copy
|
|
406
|
+
cannot be opened. `publishable: false` comes with a `problem` sentence (the data
|
|
407
|
+
folder was restored or moved so the draft names another live app; its files are
|
|
408
|
+
missing; it cannot be opened): show it and offer Discard (the only way out).
|
|
409
|
+
`recovery: 'rolled-back'` means a restart interrupted a publish and it was
|
|
410
|
+
undone; the draft is still open, publish again.
|
|
411
|
+
|
|
412
|
+
`PublishPreview = { digest, risky: true, fromVersion, toVersion, versions:
|
|
413
|
+
{ version, draftVersion, author, summary, steps, statements }[], definition:
|
|
414
|
+
DefinitionDiff, archives: { values, tables }, methods: { model, name, change:
|
|
415
|
+
'added'\|'changed'\|'unchanged', description, code, ... }[] }`. EVERY method of
|
|
416
|
+
the final definition is listed in full (draft code reaches live at publish, show
|
|
417
|
+
it). Archive counts are of that moment and can drift. The publish Approval's
|
|
418
|
+
`preview` is this object plus `draft` (the name); its `action` is
|
|
419
|
+
`{ kind: 'publish-draft', draftId, draftName, digest }`. Approving applies
|
|
420
|
+
exactly that chain and **fails** (status `failed`, the draft stays open) if the
|
|
421
|
+
draft changed since. Approve through `POST /api/shell/approvals/<id>` as for any
|
|
422
|
+
Approval. A discard asked by a tool is `{ kind: 'discard-draft', draftId,
|
|
423
|
+
draftName }`. A successful publish result is `{ version, versions, summary,
|
|
424
|
+
archives, appRestartMs, draft }`.
|
|
425
|
+
|
|
426
|
+
`describe` for each copy: `GET /api/shell/describe?target=live` is
|
|
427
|
+
`{ mode: 'live', frozen: true, draft: DraftInfo, ... }` while a draft is open;
|
|
428
|
+
`?target=draft` is `{ mode: 'draft', frozen: false, draft: DraftInfo, ... }`;
|
|
429
|
+
with no draft `draft` is null and `frozen` false. A view of live therefore has
|
|
430
|
+
what it needs for the draft strip from the same call.
|
|
431
|
+
|
|
432
|
+
History: `GET /api/shell/history` (live) tags every version a publish made with
|
|
433
|
+
`publish: { draftId, name }` (group rows by `draftId`; "Undo this publish" is
|
|
434
|
+
`POST /api/shell/undo-to { version: <parent of the group's first row> }`).
|
|
435
|
+
`?target=draft` lists only changes made in the draft (the live base version is
|
|
436
|
+
left out).
|
|
437
|
+
|
|
438
|
+
Events (`GET /api/events`) now say which copy changed:
|
|
439
|
+
`{ type: 'changed', target: 'live'|'draft', version, summary }`,
|
|
440
|
+
`{ type: 'records', target, model }` (reload only the view of that copy) and
|
|
441
|
+
`{ type: 'draft', status: 'started'|'published'|'discarded'|'unpublishable', id, name }`
|
|
442
|
+
(everything is stale: reload describe, history, lists).
|
|
443
|
+
|
|
444
|
+
### Tools and MCP
|
|
445
|
+
|
|
446
|
+
Tools: `app_startdraft` (`name?`), `app_previewpublish`, `app_publishdraft`
|
|
447
|
+
(`expect?`; always a `pending_approval`), `app_discarddraft` (a
|
|
448
|
+
`pending_approval`), `app_undoto` (`version`). `/mcp` names its copy in the URL,
|
|
449
|
+
fixed per endpoint: `/mcp` is live (builder tools are refused while a draft is
|
|
450
|
+
open; record tools work), `/mcp?target=draft` is the draft, and
|
|
451
|
+
`/mcp?scope=assistant` is the assistant's split, resolved per call. In code,
|
|
452
|
+
`callTool(principal, name, args, scope)` takes a
|
|
453
|
+
`CallScope = { records, structure }` (`scopeFor('draft')` puts both on the
|
|
454
|
+
draft); the default is live.
|
|
455
|
+
|
|
456
|
+
### The assistant
|
|
457
|
+
|
|
458
|
+
All engines (API key, ChatGPT plan, Claude Code, Codex) use the same scope
|
|
459
|
+
(`assistant/scope.ts`), resolved at every call. No draft: everything is live,
|
|
460
|
+
approvals as before. A draft open: builder tools go to the draft with no
|
|
461
|
+
per-change approval and record tools to the copy the person is viewing (the
|
|
462
|
+
saved view). The system prompt and every turn input carry the current mode (including resumed CLI sessions); stored user messages stay unchanged. `tool_result` events
|
|
463
|
+
carry `copy: 'live'|'draft'` while a draft is open, and a turn that changed
|
|
464
|
+
structure in the draft closes with "I made that change in the draft ... so the
|
|
465
|
+
live app is unchanged". Publishing through the assistant raises the publish
|
|
466
|
+
Approval and holds the turn; it never applies.
|
|
467
|
+
|
|
468
|
+
## Draft then publish (engine API)
|
|
469
|
+
|
|
470
|
+
The engine can run a second host, a draft, over a copy of the live data and
|
|
471
|
+
later replay the draft's changes onto live. Rules and failure behaviour are in
|
|
472
|
+
[agents/engine.md](agents/engine.md); the API on `AppHost` (exported from
|
|
473
|
+
`src/engine`):
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
const live = await AppHost.open(dir, { store });
|
|
477
|
+
const draft = await live.forkDraft(draftDir, { store: emptyDraftStore }); // autoApprove on
|
|
478
|
+
live.setFrozen(true); // live keeps serving records; structural commands are refused
|
|
479
|
+
// ...build in `draft` (draft.execute / draft.create ...)...
|
|
480
|
+
const preview = await live.previewPublish(draft); // diff, archives, all method code, digest
|
|
481
|
+
await live.publishDraft(draft, {
|
|
482
|
+
author, approved: true, expect: preview.digest, draftName: 'Spring cleanup',
|
|
483
|
+
}); // N ordinary live versions; any failure leaves live's app.db untouched
|
|
484
|
+
live.setFrozen(false);
|
|
485
|
+
await draft.discard(); // closes it and deletes only the directory fork made
|
|
486
|
+
await live.execute({ type: 'undoTo', version: base, from: live.version }, { author });
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
`AppHost.open(dir, { store, autoApprove: true })` only works on a directory the
|
|
490
|
+
engine marked as a draft. A host start reconciles an interrupted publish
|
|
491
|
+
(`host.recovery`). A `VersionStore` may implement the optional atomic
|
|
492
|
+
`commit(records, current)`.
|
|
493
|
+
|
|
157
494
|
## First-run schema
|
|
158
495
|
|
|
159
496
|
`prepareShellSchema(db)` (`src/lib/server/schema.ts`) creates the tables of every
|
|
@@ -172,3 +509,21 @@ pnpm --filter @happyvertical/smrt-app-shell typecheck
|
|
|
172
509
|
```
|
|
173
510
|
|
|
174
511
|
See [AGENTS.md](AGENTS.md) for the invariants and [agents/engine.md](agents/engine.md) for the engine.
|
|
512
|
+
|
|
513
|
+
### Draft UI
|
|
514
|
+
|
|
515
|
+
The content header offers **Start draft**, then **Live / Draft**, the unpublished
|
|
516
|
+
change count, **Discard** (confirmed), and **Review and publish**. Starting a draft
|
|
517
|
+
opens its view; switching copies saves the owner's choice and returns home. In
|
|
518
|
+
Draft, a warning explains that test records are thrown away and draft-only models
|
|
519
|
+
have a **new** marker. Chat shows **Building in draft**; tool chips name the copy,
|
|
520
|
+
including after reopening a thread.
|
|
521
|
+
|
|
522
|
+
`/draft/review` rehearses the publish and shows definition changes, conversions,
|
|
523
|
+
archive counts, and all final method code. **Publish** requests a digest-pinned
|
|
524
|
+
Approval; the existing approval card resolves it. A changed-chain failure offers
|
|
525
|
+
**Review again**. Counts can drift as live records change. History groups each
|
|
526
|
+
publish under its draft name; **Undo this publish** returns to its first parent
|
|
527
|
+
(and takes back subsequent changes), with an Approval if needed. Lifecycle events
|
|
528
|
+
refresh the bar and pages without reloading. An unusable draft explains the problem
|
|
529
|
+
and keeps Discard available. See [UI validation](agents/draft-ui.md).
|