@jskit-ai/agent-docs 0.1.245 → 0.1.247

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 (35) hide show
  1. package/guide/agent/app-extras/assistant-conversation.md +10 -4
  2. package/guide/agent/app-extras/realtime.md +8 -4
  3. package/guide/agent/app-setup/multi-homing.md +189 -1
  4. package/package.json +1 -1
  5. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  6. package/patterns/minimal-foundation/example/package.json +3 -3
  7. package/patterns/shell-foundation/example/package.json +4 -4
  8. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  9. package/reference/autogen/KERNEL_MAP.md +2 -0
  10. package/reference/autogen/PATTERN_INDEX.md +37 -37
  11. package/reference/autogen/packages/assistant-core.md +1 -1
  12. package/reference/autogen/packages/database-runtime.md +8 -1
  13. package/reference/autogen/packages/kernel.md +2 -0
  14. package/reference/autogen/packages/realtime.md +1 -0
  15. package/reference/autogen/packages/workspaces-core.md +4 -3
  16. package/reference/autogen/packages/workspaces-web.md +25 -2
  17. package/skills/jskit/references/multi-homing.md +189 -1
  18. package/skills/jskit/references/pattern-index.md +37 -37
  19. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +3 -3
  20. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +4 -4
  21. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  22. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +2 -2
  23. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  24. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  25. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  26. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/package.json +2 -2
  27. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +1 -1
  28. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +1 -1
  29. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  30. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  31. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +2 -2
  32. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +3 -3
  33. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +6 -0
  34. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +10 -2
  35. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +7 -0
@@ -725,8 +725,11 @@ actor may recover that conversation. The supplied runtime binding derives this
725
725
  from its existing access-denial state; a 401/403 still clears private history and
726
726
  drafts and offers no error-only Reload. Restore access or sign in through the
727
727
  application before reopening. A subscription transport failure says **Chat
728
- updates could not reconnect. Reload chat to try again.** and retains its original
729
- error as `cause`; provider errors keep their own wording.
728
+ updates could not reconnect. Retrying automatically; Reload chat to try now.** and
729
+ retains its original error as `cause`; provider errors keep their own wording.
730
+ After a transport failure or a 5xx subscription response, the subscription retries
731
+ by itself after 1 s, doubling up to 30 s, while the socket stays connected; Reload,
732
+ a reconnect or disposal replaces that pending retry. Denials (4xx) never retry.
730
733
 
731
734
  When `runtime.logger` is supplied, subscriptions emit structured
732
735
  `assistant.conversation.subscription` records containing `stage`, `previousStage`,
@@ -4121,8 +4124,11 @@ model keeps calling native tools that the conversation was not granted stops wit
4121
4124
  a visible failure and keeps the authored message instead of running without an answer. The common driver's internal configuration
4122
4125
  uses that same server startup, authentication, version, observation and cleanup
4123
4126
  implementation. `openCodeProcessEnvironment(baseEnv, { cacheRoot, dbPath,
4124
- inlineConfig, outputTokenMax, password })` assembles native flags, cache,
4125
- loopback proxy bypass and supplied inline configuration. A managed host supplies
4127
+ inlineConfig, outputTokenMax, packageInstalls, password })` assembles native
4128
+ flags, cache, loopback proxy bypass and supplied inline configuration.
4129
+ `packageInstalls: false` is for short-lived servers that need no packages, such
4130
+ as a catalogue read: OpenCode's background plugin install then fails at once, so
4131
+ it cannot hold the server alive after its first SIGTERM. A managed host supplies
4126
4132
  its isolated environment, credential roots, approved plugin configuration and
4127
4133
  application command hooks around those native values. That boundary preserves
4128
4134
  the host's existing permission and process policies while using the same native
@@ -90,10 +90,14 @@ normal component and placement APIs. Installing realtime does not append it to
90
90
  the shell.
91
91
 
92
92
  The standard status indicator is a button while disconnected. Clicking it starts
93
- a fresh connection attempt. The client also retries server-initiated disconnects
94
- and rejected handshakes, backing off from one second to at most thirty seconds.
95
- Those extra retries pause while the page is hidden or offline and resume when it
96
- becomes available. Socket.IO continues to own ordinary transport reconnection.
93
+ a fresh connection attempt at once, even while Socket.IO is waiting out a
94
+ reconnection delay; application reconnect controls use `reconnectSocketIoClient`
95
+ from `@jskit-ai/realtime/client/runtime` for the same behavior. The client also
96
+ retries server-initiated disconnects and rejected handshakes, backing off from
97
+ one second to at most thirty seconds. Those extra retries pause while the page
98
+ is hidden or offline. When it becomes visible or online again, a disconnected
99
+ socket reconnects immediately instead of finishing a delay scheduled before the
100
+ device slept. Socket.IO continues to own ordinary transport reconnection.
97
101
  Explicit client disconnection and `reconnection: false` remain respected.
98
102
 
99
103
  Every attempt goes through normal authentication. Reconnection does not restore
@@ -26,6 +26,70 @@ Choose whether the product is:
26
26
  Do not add workspace machinery to a product that only needs an owner id on one
27
27
  domain record.
28
28
 
29
+ Set the model with `tenancyMode` and `tenancyPolicy` in `config/public.js`:
30
+
31
+ | `tenancyMode` | Workspaces | First workspace | Creating more |
32
+ | --- | --- | --- | --- |
33
+ | `none` | None | None | No |
34
+ | `personal` | One personal workspace per user | Always provisioned | No |
35
+ | `workspaces` | Named workspaces with membership, invitations and switching | Only with `tenancyPolicy.workspace.autoProvision: true` | With `tenancyPolicy.workspace.allowSelfCreate: true` |
36
+
37
+ In `workspaces` mode the product chooses one of two styles:
38
+
39
+ - **Team-first** (the default, `autoProvision` unset or `false`): a new account
40
+ has no workspace until it creates one or accepts an invitation. Mount the
41
+ **Your workspaces** page (see [Browser composition](#browser-composition)) so
42
+ these people can do either.
43
+ - **Personal workspace plus shared workspaces** (`autoProvision: true`): every
44
+ user, including people who signed up through an invitation, owns one personal
45
+ workspace and can also join or create others.
46
+
47
+ ```js
48
+ // config/public.js
49
+ config.tenancyMode = "workspaces";
50
+ config.tenancyPolicy = {
51
+ workspace: {
52
+ allowSelfCreate: true,
53
+ autoProvision: true
54
+ }
55
+ };
56
+ ```
57
+
58
+ Provisioning runs in the users profile-sync lifecycle after registration and
59
+ sign-in. The first run creates the personal workspace, its owner membership and
60
+ its settings; later runs reuse it. The default name is the user's display name,
61
+ or email, followed by "'s Workspace". Set `personalWorkspaceName` in
62
+ `config/server.js` to choose another; it receives the synchronized profile
63
+ (`id`, `displayName`, `email`, `username`), and an empty result keeps the default:
64
+
65
+ ```js
66
+ // config/server.js
67
+ config.personalWorkspaceName = (user) => `${user.displayName || user.email}'s home`;
68
+ ```
69
+
70
+ ### Development sign-in
71
+
72
+ Development sign-in (`POST /api/dev-auth/login-as`, used by the Playwright
73
+ `loginAsExistingUser()` helper and managed previews) signs in as an existing
74
+ user without changing data. It does not run the profile-sync lifecycle, so
75
+ workspace provisioning and other lifecycle contributors do not run. Create the
76
+ workspaces that seeded users need in seed data, or sign in normally once.
77
+
78
+ ## Data model
79
+
80
+ The package migrations create and own these tables:
81
+
82
+ | Table | Columns that applications use |
83
+ | --- | --- |
84
+ | `workspaces` | `id`, `slug` (unique), `name`, `owner_user_id`, `is_personal`, `avatar_url`, `deleted_at` |
85
+ | `workspace_memberships` | `workspace_id`, `user_id`, `role_sid`, `status`; one row per workspace and user |
86
+ | `workspace_settings` | One row per workspace keyed by `workspace_id`: the eight palette colours and `invites_enabled` |
87
+ | `workspace_invites` | `workspace_id`, `email`, `role_sid`, `status`, `token_hash`, `invited_by_user_id`, `expires_at`, `accepted_at`, `revoked_at` |
88
+
89
+ Application records that belong to a workspace use a non-null `workspace_id`
90
+ foreign key to `workspaces.id`. Read and change these tables through the
91
+ `workspaces.core` repositories and services, not directly.
92
+
29
93
  ## Server composition
30
94
 
31
95
  Use `workspaces/workspace-server` for role vocabulary, creation policy,
@@ -35,6 +99,103 @@ Workspace scope must reach repository queries. Parsing a workspace slug or
35
99
  hiding an admin link is not authorization. Membership and role checks happen
36
100
  before an operation discloses records or record existence.
37
101
 
102
+ ### Workspace-scoped features and routes
103
+
104
+ For a standard CRUD resource owned by a workspace, give its table the
105
+ `workspace_id` column, set the resource's `autofilter: "workspace"`, and bind
106
+ `defineCrudJsonApiFeature()` with `ownershipFilter: "workspace"` on a workspace
107
+ surface. JSKIT then filters `workspaceId` by the workspace in the route.
108
+ `workspace_user` also filters by the signed-in user. See the CRUD operations
109
+ guide.
110
+
111
+ A hand-written route registers under `/api/w/:workspaceSlug/...` with
112
+ `contextPolicy: "required"`:
113
+
114
+ ```js
115
+ router.register(
116
+ "GET",
117
+ "/api/w/:workspaceSlug/projects",
118
+ {
119
+ auth: "required",
120
+ surface: "app",
121
+ contextPolicy: "required",
122
+ permission: "projects.view" // optional
123
+ },
124
+ async function (request, reply) {
125
+ const projects = await projectsService.listForWorkspace(request.workspace.id);
126
+ // ...
127
+ }
128
+ );
129
+ ```
130
+
131
+ Before the handler runs, the auth plugin resolves the slug for the signed-in
132
+ user and sets `request.workspace`, `request.membership` (including `roleSid`
133
+ and `status`) and `request.permissions`. An unknown slug returns 404 and a
134
+ non-member receives 403, unless the route's `surface` uses an access policy with
135
+ `requireWorkspaceMembership: false`. A route with `permission` returns 403 when
136
+ the membership lacks it. Scope queries with `request.workspace.id`; never take a
137
+ workspace id from request input.
138
+
139
+ ### Workspace services from another feature
140
+
141
+ Declare `workspaces: "workspaces.core"` in the feature's `requires`. The
142
+ capability provides:
143
+
144
+ | Member | Use |
145
+ | --- | --- |
146
+ | `workspaces.services.directory` | Workspace lists, context by slug, creation and provisioning |
147
+ | `workspaces.services.members` | Members, roles and invitations |
148
+ | `workspaces.services.pendingInvitations` | Invitation resolution, redemption and acceptance participants |
149
+ | `workspaces.services.settings` | Workspace settings |
150
+ | `workspaces.repositories.workspaces`, `.workspaceMemberships`, `.workspaceSettings`, `.workspaceInvites` | Repository access inside the application's own transactions |
151
+ | `workspaces.tenancyProfile` | The resolved tenancy mode and workspace policy |
152
+
153
+ ### Invitation email
154
+
155
+ To send invitation email, set a mailer in `config/server.js`. Without one,
156
+ invitations are still created and delivery reports `mailer_unconfigured`.
157
+ `workspaceInviteMailer` is an object whose `sendWorkspaceInvite(message,
158
+ options)` receives `{ email, inviteUrl, workspace, inviter, roleSid, expiresAt,
159
+ message }`, where the inner `message` is the rendered `{ subject, text, html }`.
160
+ `workspaceInviteEmailTemplate` is an optional function that renders that inner
161
+ message; it receives `{ email, inviteUrl, workspace, inviter, roleSid,
162
+ roleLabel, expiresAt }` (`roleLabel` only when configured). Invitation links use `/invite/:token` unless
163
+ `workspaceInvitations.invitePath` names another path. The
164
+ `workspaces/workspace-server` pattern includes an example renderer.
165
+
166
+ ### Workspace colours
167
+
168
+ Each workspace stores a light and a dark palette in `workspace_settings`. On
169
+ workspace routes, `workspaces-web` switches Vuetify to the `workspace-light` or
170
+ `workspace-dark` theme, built from the application's own theme with `primary`,
171
+ `secondary`, `surface` and `surface-variant` replaced by the workspace's
172
+ colours. Other routes use the application's themes.
173
+
174
+ New workspaces start from JSKIT's blue palette unless `config/server.js` sets
175
+ the application's own. Missing fields keep JSKIT's values; an invalid colour
176
+ stops the server at startup:
177
+
178
+ ```js
179
+ // config/server.js
180
+ config.workspaceSettings = {
181
+ defaults: {
182
+ invitesEnabled: true,
183
+ lightPrimaryColor: "#1C1C1C",
184
+ lightSecondaryColor: "#5C5C5C",
185
+ lightSurfaceColor: "#FFFFFF",
186
+ lightSurfaceVariantColor: "#424242",
187
+ darkPrimaryColor: "#F5F5F5",
188
+ darkSecondaryColor: "#BDBDBD",
189
+ darkSurfaceColor: "#121212",
190
+ darkSurfaceVariantColor: "#C8C8C8"
191
+ }
192
+ };
193
+ ```
194
+
195
+ Defaults apply when a workspace is created. Existing workspaces keep their
196
+ stored colours; change them with `PATCH /api/w/:workspaceSlug/settings` or
197
+ `workspaces.services.settings.updateWorkspaceSettings(workspace, patch)`.
198
+
38
199
  `workspace_authenticated` and similar policies must implement their documented
39
200
  membership semantics exactly; do not synthesize memberships to compensate for
40
201
  a policy/runtime mismatch.
@@ -265,7 +426,34 @@ placements.
265
426
 
266
427
  The route slug, selected workspace, query keys, and server scope stay aligned.
267
428
  Invalid workspace URLs show a deliberate not-found/forbidden state instead of
268
- silently selecting another workspace.
429
+ silently selecting another workspace. The browser waits for the server's answer
430
+ about an unknown slug before deciding, so the surface can show that state.
431
+
432
+ ### Surfaces and where `/` lands
433
+
434
+ Workspace surfaces put the slug in their pages root, such as
435
+ `pagesRoot: "w/[workspaceSlug]"` with `requiresWorkspace: true`, and
436
+ `w/[workspaceSlug]/admin` for administration. Keep the default surface
437
+ (`surfaceDefaultId`, usually `home`) free of a workspace requirement: `/`,
438
+ invitation links and other unscoped pages belong to it. With
439
+ `webRootAllowed: "no"`, `/` opens that surface's root.
440
+
441
+ `workspaces-web` remembers the last workspace each person opened in this
442
+ browser. The **Your workspaces** page, `WorkspacesClientElement` from
443
+ `@jskit-ai/workspaces-web/client/components/WorkspacesClientElement`, uses it:
444
+
445
+ 1. With pending invitations, it shows them so the person can join or refuse.
446
+ 2. Otherwise it opens the last workspace used in this browser, if the person is
447
+ still a member, or their only workspace.
448
+ 3. Otherwise it lists their workspaces and, when they have none and
449
+ `allowSelfCreate` is on, offers to create the first one.
450
+
451
+ The page sends signed-out visitors to sign in. The
452
+ `workspaces/workspace-surfaces` pattern mounts it at `/home/workspaces`. The
453
+ product decides where people land: keep a public landing page at `/home` that
454
+ links signed-in people there, or mount the page at `/home` itself so everyone
455
+ goes straight to sign-in or into their workspace. Applications may replace it
456
+ with their own page.
269
457
 
270
458
  Loading uses skeletons; mutation errors use toasts. Warm-cache and browser
271
459
  back/forward navigation must restore the correct workspace context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.245",
3
+ "version": "0.1.247",
4
4
  "description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -5,7 +5,7 @@
5
5
  "private": true,
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@jskit-ai/kernel": "0.1.246",
8
+ "@jskit-ai/kernel": "0.1.248",
9
9
  "json-rest-schema": "^1.0.17"
10
10
  },
11
11
  "exports": {
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@local/main": "0.1.0",
36
36
  "@fastify/static": "^10.1.3",
37
- "@jskit-ai/kernel": "0.1.246",
37
+ "@jskit-ai/kernel": "0.1.248",
38
38
  "@tanstack/vue-query": "^5.101.0",
39
39
  "fastify": "^5.8.5",
40
40
  "json-rest-schema": "^1.0.17",
@@ -42,13 +42,13 @@
42
42
  "vue": "^3.5.44",
43
43
  "vue-router": "^5.3.1",
44
44
  "vuetify": "^4.1.2",
45
- "@jskit-ai/http-runtime": "0.1.244",
45
+ "@jskit-ai/http-runtime": "0.1.246",
46
46
  "@fastify/fast-json-stringify-compiler": "^5.1.0",
47
47
  "@vue/devtools-api": "^8.2.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@jskit-ai/config-eslint": "0.1.241",
51
- "@jskit-ai/jskit-catalog": "0.1.298",
51
+ "@jskit-ai/jskit-catalog": "0.1.300",
52
52
  "@playwright/test": "1.61.1",
53
53
  "@vitejs/plugin-vue": "^6.0.7",
54
54
  "eslint": "^10.8.0",
@@ -34,7 +34,7 @@
34
34
  "dependencies": {
35
35
  "@local/main": "0.1.0",
36
36
  "@fastify/static": "^10.1.3",
37
- "@jskit-ai/kernel": "0.1.246",
37
+ "@jskit-ai/kernel": "0.1.248",
38
38
  "@tanstack/vue-query": "^5.101.0",
39
39
  "fastify": "^5.8.5",
40
40
  "json-rest-schema": "^1.0.17",
@@ -42,15 +42,15 @@
42
42
  "vue": "^3.5.44",
43
43
  "vue-router": "^5.3.1",
44
44
  "vuetify": "^4.1.2",
45
- "@jskit-ai/http-runtime": "0.1.244",
45
+ "@jskit-ai/http-runtime": "0.1.246",
46
46
  "@mdi/js": "^7.4.47",
47
- "@jskit-ai/shell-web": "0.1.253",
47
+ "@jskit-ai/shell-web": "0.1.255",
48
48
  "@fastify/fast-json-stringify-compiler": "^5.1.0",
49
49
  "@vue/devtools-api": "^8.2.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@jskit-ai/config-eslint": "0.1.241",
53
- "@jskit-ai/jskit-catalog": "0.1.298",
53
+ "@jskit-ai/jskit-catalog": "0.1.300",
54
54
  "@playwright/test": "1.61.1",
55
55
  "@vitejs/plugin-vue": "^6.0.7",
56
56
  "eslint": "^10.8.0",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "description": "App-local runtime composition and lightweight glue.",
11
11
  "dependencies": {
12
- "@jskit-ai/kernel": "0.1.246"
12
+ "@jskit-ai/kernel": "0.1.248"
13
13
  },
14
14
  "jskit": {
15
15
  "kind": "runtime",
@@ -81,6 +81,8 @@ Exports
81
81
  - `normalizeQueryToken(value, { fallback = "__none__" } = {})`
82
82
  - `normalizeObject(value, { fallback = {} } = {})`
83
83
  - `normalizeIfInSource(source, normalized, fieldName, normalizer = (value) => value)`
84
+ - `normalizeIfPresent(value, normalizer = (entry) => entry)`
85
+ - `normalizeOrNull(value, normalizer = (entry) => entry)`
84
86
  - `isRecord(value)`
85
87
  - `normalizeArray(value)`
86
88
  - `normalizeUniqueTextList(value, { acceptSingle = false } = {})`