@genesislcap/blank-app-seed 5.27.2-prerelease.3 → 5.28.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/.genx/ai-consumer.json +18 -0
- package/.genx/configure.js +122 -0
- package/.genx/package.json +1 -1
- package/.genx/scripts/check-ai-declaration.mjs +116 -0
- package/.genx/scripts/check-ai-emission.sh +974 -0
- package/.genx/scripts/generate-test-apps.sh +38 -4
- package/.genx/templates/react/ai/assistant-host.ts.hbs +30 -0
- package/.genx/templates/react/ai/assistant.ts.hbs +52 -0
- package/.genx/templates/react/ai/extensions.ts.hbs +17 -0
- package/.genx/templates/react/ai/pbc-elements.ts.hbs +17 -0
- package/.genx/templates/server/ai-service-web-handler.kts.hbs +394 -0
- package/.genx/tests/contracts/ai/ai-resolver-cases.json +6807 -0
- package/.genx/tests/contracts/ai/ui-config-ai.schema.json +203 -0
- package/.genx/tests/fixtures/ai-config-parse-breakers.json +16 -0
- package/.genx/tests/fixtures/ai-config.json +41 -0
- package/.genx/versions.json +3 -3
- package/.github/workflows/build.yml +20 -4
- package/CHANGELOG.md +55 -43
- package/README.md +139 -0
- package/bdd-tests/build.gradle.kts +1 -1
- package/bdd-tests/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/bdd-tests/gradle.properties +0 -1
- package/bdd-tests/settings.gradle.kts +2 -2
- package/client-tmp/angular/package.json +3 -0
- package/client-tmp/react/.oxfmtrc.json +3 -0
- package/client-tmp/react/package.json +6 -2
- package/client-tmp/web-components/package.json +3 -0
- package/client-tmp/web-components/settings.gradle.kts +0 -22
- package/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/package.json +1 -1
- package/server/build.gradle.kts +8 -1
- package/server/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/server/gradle.properties +3 -3
- package/server/settings.gradle.kts +2 -2
- package/server/{{appName}}-app/src/main/genesis/scripts/genesis-router.kts +10 -0
- package/server/{{appName}}-app/src/test/kotlin/global/genesis/EventHandlerTest.kt +1 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
|
3
|
+
"$id": "https://genesis.global/schemas/create/ui-config-ai.schema.json",
|
|
4
|
+
"title": "ui.config.ai",
|
|
5
|
+
"version": "1.6.0",
|
|
6
|
+
"description": "The AI chat block a generated app reads from ui.config (GENC-1601 contract C-8). Produced by the archive service's ai-resolver from the project payload, and copied VERBATIM into the seed — both sides pin this file's digest, so a change here is a deliberate version bump, not an edit. It carries WHAT the chat may do and nothing about WHERE it talks: no url, endpoint, key or budget field exists, and additionalProperties is false everywhere so one cannot be added by accident. That is load-bearing — this block is written into the generated app's genesis-create.json and ships in the exported project, so a Create endpoint or key here would leave the box.",
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"required": ["enabled", "vendor", "tier", "systemPrompt", "resources"],
|
|
10
|
+
"properties": {
|
|
11
|
+
"enabled": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"description": "Emitted only for a project that selected AI chat; the block is absent entirely otherwise, so a project without AI stays byte-identical (C57)."
|
|
14
|
+
},
|
|
15
|
+
"vendor": {
|
|
16
|
+
"enum": ["gemini", "anthropic"],
|
|
17
|
+
"description": "Default 'gemini' (R-7). The generated app resolves a provider for it; the endpoint is supplied by the build environment, never from here."
|
|
18
|
+
},
|
|
19
|
+
"tier": {
|
|
20
|
+
"enum": ["low", "high", "reasoning"],
|
|
21
|
+
"description": "Default 'high' (R-7). Maps to a model through the foundation-ui tier table (C-3), so no model id appears in a generated project."
|
|
22
|
+
},
|
|
23
|
+
"systemPrompt": {
|
|
24
|
+
"type": "string",
|
|
25
|
+
"minLength": 1,
|
|
26
|
+
"description": "The assistant's standing instruction. Derived deterministically from the project, so regenerating an unchanged project produces an unchanged file."
|
|
27
|
+
},
|
|
28
|
+
"resources": {
|
|
29
|
+
"type": "array",
|
|
30
|
+
"uniqueItems": true,
|
|
31
|
+
"description": "What the chat may read and call, by the name the platform actually registers. A name that matches nothing is dropped silently by the runtime (C34), so these are derived from the project rather than typed by hand. One entry per name: the chat tolerates a duplicate, but MCP's enableMcp refuses the whole script over one, so the resolver dedupes and both consumers can trust it.",
|
|
32
|
+
"items": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"required": ["name", "kind", "context"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"name": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"pattern": "^[A-Z][A-Z0-9_]*$",
|
|
40
|
+
"maxLength": 64,
|
|
41
|
+
"description": "REQ_<NAME> for a request-server resource, EVENT_<NAME> for an event handler, and a dataserver query's own name, which is neither (C-15A.5 K-2). Never the bare MCP resource name (C34). At most 64 characters: the assistant's tool name is this name lowercased, and vendors reject longer tool names."
|
|
42
|
+
},
|
|
43
|
+
"kind": {
|
|
44
|
+
"enum": ["request", "event", "query"],
|
|
45
|
+
"description": "'request' is a read through the request server; 'event' is a write through an event handler; 'query' is a read of a dataserver query's live rows (C-15). A seed passes 'query' on only when its UI reads it (C-15A.5)."
|
|
46
|
+
},
|
|
47
|
+
"op": {
|
|
48
|
+
"enum": ["insert", "modify", "delete", "custom"],
|
|
49
|
+
"description": "Events only, in the platform's WIRE vocabulary. The project meta says create/update/delete; the resolver maps create→insert and update→modify so nothing downstream knows two names for one thing."
|
|
50
|
+
},
|
|
51
|
+
"context": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"minLength": 1,
|
|
54
|
+
"description": "One line telling the model what this resource is for."
|
|
55
|
+
},
|
|
56
|
+
"maxRows": {
|
|
57
|
+
"type": "integer",
|
|
58
|
+
"minimum": 1,
|
|
59
|
+
"maximum": 1000,
|
|
60
|
+
"description": "Requests and queries only; the resolver emits 50. A row cap in the host bridge applies regardless — this is guidance, not enforcement."
|
|
61
|
+
},
|
|
62
|
+
"shape": {
|
|
63
|
+
"enum": ["row"],
|
|
64
|
+
"description": "Custom events only (C-18.3). 'row': the handler's code was proven to change one existing row of entity, looked up by its primary key, and optionally insert rows elsewhere. A new meaning gets a new value, never a looser 'row'."
|
|
65
|
+
},
|
|
66
|
+
"entity": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"pattern": "^[A-Z0-9_]+$",
|
|
69
|
+
"description": "The table a resource acts on. Allowed on any resource with that one meaning (C-8); required with shape, where the bridge reads the row through REQ_<entity>."
|
|
70
|
+
},
|
|
71
|
+
"key": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"minItems": 1,
|
|
74
|
+
"uniqueItems": true,
|
|
75
|
+
"items": { "type": "string", "pattern": "^[A-Z0-9_]+$", "not": { "enum": ["RECORD_ID", "TIMESTAMP"] } },
|
|
76
|
+
"description": "The table's primary-key fields, in primary-key order. On a row action, the row the handler loads is the row the review shows; on a modify or a delete, the row the write names. RECORD_ID and TIMESTAMP are the platform's own and never a key."
|
|
77
|
+
},
|
|
78
|
+
"inputs": {
|
|
79
|
+
"type": "array",
|
|
80
|
+
"items": {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"additionalProperties": false,
|
|
83
|
+
"required": ["field", "required"],
|
|
84
|
+
"properties": {
|
|
85
|
+
"field": { "type": "string", "pattern": "^[A-Z0-9_]+$" },
|
|
86
|
+
"required": { "const": true }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"description": "The fields the handler's code reads from the request beyond the key. May be empty. Every input is required in v1, and the constant is written so the bridge checks it rather than assumes it."
|
|
90
|
+
},
|
|
91
|
+
"effects": {
|
|
92
|
+
"type": "array",
|
|
93
|
+
"minItems": 1,
|
|
94
|
+
"items": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"additionalProperties": false,
|
|
97
|
+
"required": ["op", "table"],
|
|
98
|
+
"properties": {
|
|
99
|
+
"op": { "enum": ["modify", "insert"] },
|
|
100
|
+
"table": { "type": "string", "pattern": "^[A-Z0-9_]+$" }
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"contains": { "required": ["op"], "properties": { "op": { "const": "modify" } } },
|
|
104
|
+
"minContains": 1,
|
|
105
|
+
"maxContains": 1,
|
|
106
|
+
"description": "One per write call in the handler's code, in code order: exactly one modify, of entity, and any inserts."
|
|
107
|
+
},
|
|
108
|
+
"customCode": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"additionalProperties": false,
|
|
111
|
+
"required": ["alsoWrites", "listComplete"],
|
|
112
|
+
"properties": {
|
|
113
|
+
"alsoWrites": {
|
|
114
|
+
"type": "array",
|
|
115
|
+
"uniqueItems": true,
|
|
116
|
+
"items": { "type": "string", "pattern": "^[A-Z0-9_]+$" },
|
|
117
|
+
"description": "Tables other than its own that the op's custom code provably writes to, in the order it first writes each. A floor, never the whole story on its own."
|
|
118
|
+
},
|
|
119
|
+
"listComplete": {
|
|
120
|
+
"type": "boolean",
|
|
121
|
+
"description": "True only when every write, call and way out of the code was accounted for, so alsoWrites names every other table it writes."
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"description": "A CRUD event whose handler runs custom code (C-18.D): the review tells the user that the code can do more than save the values shown."
|
|
125
|
+
},
|
|
126
|
+
"references": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"additionalProperties": false,
|
|
131
|
+
"required": ["resource", "fields"],
|
|
132
|
+
"properties": {
|
|
133
|
+
"resource": { "type": "string", "pattern": "^REQ_[A-Z0-9_]+$" },
|
|
134
|
+
"fields": {
|
|
135
|
+
"type": "array",
|
|
136
|
+
"minItems": 1,
|
|
137
|
+
"items": {
|
|
138
|
+
"type": "object",
|
|
139
|
+
"additionalProperties": false,
|
|
140
|
+
"required": ["field", "targetField"],
|
|
141
|
+
"properties": {
|
|
142
|
+
"field": { "type": "string", "pattern": "^[A-Z0-9_]+$" },
|
|
143
|
+
"targetField": { "type": "string", "pattern": "^[A-Z0-9_]+$" }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"description": "Declared references of this write, for the bridge's existence check. Absent on a config older than this version, when the app's seed does not declare references, when an upstream pre-resolved block omits it, or when Create had no fields list for the table; [] means the table declares none. Only an insert or modify event carries it. Two rules the schema cannot say: a field appears at most once across all groups, and a group's targetField values are distinct; the resolver guarantees both by construction and its resourceProblem checks them on the pre-resolved path."
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
"$comment": "The kind decides the rest, and the name's prefix must fit it, as in the foundation-ui assistant's validateGenesisAiConfig, which blocks the WHOLE assistant on any mismatch. These rules mirror it, with two things a schema cannot say: two resources sharing a name but differing in another field pass uniqueItems (it only rejects identical items) yet block the assistant, which is why the resolver dedupes by name; and enabled false is valid here because it is a deliberate off, not a block the assistant will run. A row action (shape) has three more the schema cannot say, enforced by the resolver and mirrored by the bridge's rowActionProblem: the modify effect's table is entity; no key field is also an input; and REQ_<entity> is a request resource in the same block.",
|
|
153
|
+
"allOf": [
|
|
154
|
+
{
|
|
155
|
+
"description": "A request is a REQ_ name, and no op.",
|
|
156
|
+
"if": { "properties": { "kind": { "const": "request" } } },
|
|
157
|
+
"then": { "properties": { "name": { "pattern": "^REQ_[A-Z0-9_]+$" } }, "not": { "required": ["op"] } }
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"description": "An event is an EVENT_ name, an op, and no maxRows.",
|
|
161
|
+
"if": { "properties": { "kind": { "const": "event" } } },
|
|
162
|
+
"then": { "properties": { "name": { "pattern": "^EVENT_[A-Z0-9_]+$" } }, "required": ["op"], "not": { "required": ["maxRows"] } }
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"description": "A query is a name that is neither a request's nor an event's, and no op (C-15A.5 K-2, K-3).",
|
|
166
|
+
"if": { "properties": { "kind": { "const": "query" } } },
|
|
167
|
+
"then": { "properties": { "name": { "not": { "pattern": "^(REQ|EVENT)_" } } }, "not": { "required": ["op"] } }
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
"description": "A row action's companions ride a custom op with shape only, and with shape all four are required. key may also ride a modify or a delete (below).",
|
|
171
|
+
"if": { "required": ["op", "shape"], "properties": { "op": { "const": "custom" } } },
|
|
172
|
+
"then": { "required": ["entity", "key", "inputs", "effects"] },
|
|
173
|
+
"else": {
|
|
174
|
+
"not": {
|
|
175
|
+
"anyOf": [{ "required": ["shape"] }, { "required": ["inputs"] }, { "required": ["effects"] }]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"description": "key rides a custom op with shape (above) or a modify or delete event (C-19.1). op is required in each branch: an absent op would satisfy it vacuously.",
|
|
181
|
+
"if": { "required": ["key"] },
|
|
182
|
+
"then": {
|
|
183
|
+
"anyOf": [
|
|
184
|
+
{ "required": ["op", "shape"], "properties": { "op": { "const": "custom" } } },
|
|
185
|
+
{ "required": ["op"], "properties": { "op": { "enum": ["modify", "delete"] } } }
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"description": "customCode rides an insert, modify or delete event only. op is required here: an absent op would satisfy an if on op vacuously.",
|
|
191
|
+
"if": { "required": ["customCode"] },
|
|
192
|
+
"then": { "required": ["op"], "properties": { "op": { "enum": ["insert", "modify", "delete"] } } }
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"description": "references ride an insert or modify event only (C-17.3). op is required in the if: an absent op would satisfy it vacuously.",
|
|
196
|
+
"if": { "required": ["op"], "properties": { "op": { "enum": ["insert", "modify"] } } },
|
|
197
|
+
"else": { "not": { "required": ["references"] } }
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ai": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"vendor": "gemini",
|
|
5
|
+
"tier": "high",
|
|
6
|
+
"systemPrompt": "Text Handlebars cannot even parse: {{#if}} opens a block with no argument, {{{ never closes, and {{#each items}} is never closed.",
|
|
7
|
+
"resources": [
|
|
8
|
+
{
|
|
9
|
+
"name": "REQ_TRADE",
|
|
10
|
+
"kind": "request",
|
|
11
|
+
"context": "Trades, {{#unless}}.",
|
|
12
|
+
"maxRows": 50
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ai": {
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"vendor": "gemini",
|
|
5
|
+
"tier": "high",
|
|
6
|
+
"systemPrompt": "You are the assistant for {{appName}}{{! a comment }}. Keep \\{{this}}, {{#if AI.enabled}}shown{{/if}}, {{#if x}}hidden{{/if}} and {{{that}}} literal; \"quotes\", </script>, ünïcödé and the text \\u007b must survive.",
|
|
7
|
+
"resources": [
|
|
8
|
+
{
|
|
9
|
+
"name": "REQ_TRADE",
|
|
10
|
+
"kind": "request",
|
|
11
|
+
"context": "Trades booked in {{appName}}.",
|
|
12
|
+
"maxRows": 50
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "EVENT_TRADE_INSERT",
|
|
16
|
+
"kind": "event",
|
|
17
|
+
"op": "insert",
|
|
18
|
+
"context": "Book a new trade.",
|
|
19
|
+
"customCode": { "alsoWrites": ["POSITION"], "listComplete": false },
|
|
20
|
+
"references": [
|
|
21
|
+
{ "resource": "REQ_COUNTERPARTY", "fields": [{ "field": "COUNTERPARTY_ID", "targetField": "COUNTERPARTY_ID" }] }
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "EVENT_TRADE_MODIFY",
|
|
26
|
+
"kind": "event",
|
|
27
|
+
"op": "modify",
|
|
28
|
+
"context": "Change a trade.",
|
|
29
|
+
"references": [
|
|
30
|
+
{ "resource": "REQ_COUNTERPARTY", "fields": [{ "field": "COUNTERPARTY_ID", "targetField": "COUNTERPARTY_ID" }] }
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "REQ_COUNTERPARTY",
|
|
35
|
+
"kind": "request",
|
|
36
|
+
"context": "Counterparties a trade can name.",
|
|
37
|
+
"maxRows": 50
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
package/.genx/versions.json
CHANGED
|
@@ -12,15 +12,17 @@ concurrency:
|
|
|
12
12
|
jobs:
|
|
13
13
|
# The `build` job below covers React end to end (gradle build, lint, unit + e2e) for
|
|
14
14
|
# the defaults app. This job keeps every framework's generator honest: for each one it
|
|
15
|
-
# generates the defaults app, the full-routes fixture app and an FDC3-channels app,
|
|
16
|
-
# and runs the ox lint gates over
|
|
15
|
+
# generates the defaults app, the full-routes fixture app and an FDC3-channels app, plus
|
|
16
|
+
# for React an app with the AI chat on, and runs the ox lint gates over each.
|
|
17
17
|
#
|
|
18
18
|
# BUILD=1 adds `tsc --noEmit` and the production build per app, which is what makes a
|
|
19
19
|
# broken generated tile fail here. Lint alone does not type-check, and the app the
|
|
20
20
|
# `build` job below compiles is the defaults one, which generates no tiles at all, so
|
|
21
21
|
# before this nothing compiled a generated tile: a grid tile passed a prop the React
|
|
22
22
|
# wrapper's types did not declare and CI stayed green while `npm run build` failed for
|
|
23
|
-
# anyone scaffolding with routes.
|
|
23
|
+
# anyone scaffolding with routes. For the AI app it also checks that the assistant and
|
|
24
|
+
# its bubble are built into a chunk of their own, and into no other, and that the seed's
|
|
25
|
+
# AI declaration holds the baseline and names nothing the installed assistant does not read.
|
|
24
26
|
generated-apps:
|
|
25
27
|
strategy:
|
|
26
28
|
fail-fast: false
|
|
@@ -37,9 +39,15 @@ jobs:
|
|
|
37
39
|
with:
|
|
38
40
|
node-version: 22
|
|
39
41
|
|
|
40
|
-
- name: Generate and lint ${{ matrix.framework }} apps (default + full + fdc3)
|
|
42
|
+
- name: Generate and lint ${{ matrix.framework }} apps (default + full + fdc3, and ai for React)
|
|
41
43
|
run: ./.genx/scripts/generate-test-apps.sh ${{ matrix.framework }}
|
|
42
44
|
|
|
45
|
+
# Generates its own React and web-components apps, so one leg is enough. This job has no Java
|
|
46
|
+
# or artifactory credentials, so it runs the emission checks only; the build job adds GRADLE=1.
|
|
47
|
+
- name: Check what the AI chat path emits
|
|
48
|
+
if: matrix.framework == 'react'
|
|
49
|
+
run: ./.genx/scripts/check-ai-emission.sh
|
|
50
|
+
|
|
43
51
|
build:
|
|
44
52
|
env:
|
|
45
53
|
genesisArtifactoryUser: ${{ secrets.JFROG_LIBS_RELEASE_CLIENT_RO_USER }}
|
|
@@ -79,6 +87,14 @@ jobs:
|
|
|
79
87
|
working-directory: /tmp/testapp
|
|
80
88
|
run: ./gradlew ${GRADLE_PARAMS} build --info
|
|
81
89
|
|
|
90
|
+
# The AI chat proxy is a script, and scripts compile only when the router starts, so no build
|
|
91
|
+
# compiles it. This one builds an AI app, runs the security scan on it and compiles both
|
|
92
|
+
# vendors' proxies against this Genesis version. It picks up GRADLE_PARAMS from the job.
|
|
93
|
+
- name: Build, scan and compile the AI chat proxy
|
|
94
|
+
env:
|
|
95
|
+
GRADLE: 1
|
|
96
|
+
run: ./.genx/scripts/check-ai-emission.sh
|
|
97
|
+
|
|
82
98
|
- name: Lint UI
|
|
83
99
|
working-directory: /tmp/testapp/client
|
|
84
100
|
run: npm run lint
|
package/CHANGELOG.md
CHANGED
|
@@ -1,48 +1,60 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## [5.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
###
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
3
|
+
## [5.28.0](https://github.com/genesiscommunitysuccess/blank-app-seed/compare/v5.27.1...v5.28.0) (2026-10-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add desktop and headless Genesis Start scripts to the client GENC-1609 2806180
|
|
9
|
+
* add the AI assistant to an AI app, in a chat bubble GENC-1611 (#638) 5a04d57
|
|
10
|
+
* add the AI chat panel to an AI app, loading the assistant the first time it opens GENC-1611 b6d31f5
|
|
11
|
+
* always depend on the AI assistant in a React app, so a preview base already has it GENC-1611 393c84b
|
|
12
|
+
* copy Create's schema 1.6.0 and cases 1.8.0, with modifies and deletes named by key GENC-1611 65f4621
|
|
13
|
+
* copy to the AI config only what the seed declares its assistant reads GENC-1611 b07cf57
|
|
14
|
+
* copy to the AI config only what the seed declares its assistant reads GENC-1611 (#642) 4385268
|
|
15
|
+
* give an AI app the assistant package and build it with AI switched on GENC-1611 660ca45
|
|
16
|
+
* keep a key only where it names a row, and check a modify's key through CI GENC-1611 c238ab3
|
|
17
|
+
* move Genesis Start to 0.1.15, the first launcher that can run headless GENC-1609 81136ae
|
|
18
|
+
* open the AI assistant from a chat bubble GENC-1611 212bce1
|
|
19
|
+
* pass queries, references and row actions to the AI chat GENC-1611 (#643) 92d7c5e
|
|
20
|
+
* pin UI 15.47.0 and pass its queries and references to the chat GENC-1611 37ebdfd
|
|
21
|
+
* pin UI 15.50.0 and pass row actions and keys to the chat GENC-1611 6aafccd
|
|
22
|
+
* ship the AI chat proxy with a React app that asks for it GENC-1609 4b67a7a
|
|
23
|
+
* ship the AI chat proxy with a React app that asks for it GENC-1609 (#636) 76dabeb
|
|
24
|
+
* write a row action's fields as the chat reads them, and check them through CI GENC-1611 fa0f782
|
|
25
|
+
* write the chat panel's configuration into an AI app GENC-1611 6eff2c2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
* accept a lowercase content-type from a proxy GENC-1609 8002275
|
|
31
|
+
* allow the reasoning tier's models in the proxy GENC-1609 0a9c11d
|
|
32
|
+
* answer 424 when the AI key cannot be read, never an exception GENC-1609 6cdb268
|
|
33
|
+
* answer the chat panel as JSON whatever it accepts GENC-1609 9c3cbb7
|
|
34
|
+
* ask Google's repository only for the launcher's androidx artifacts, after Genesis's own GENC-1609 1788d9b
|
|
35
|
+
* bound what the proxy will parse from a request body GENC-1609 032f00b
|
|
36
|
+
* build the AI limits into the proxy so a rewritten sysdef cannot drop them GENC-1609 8144dec
|
|
37
|
+
* document only the environment route for the AI keys GENC-1609 f56f8e0
|
|
38
|
+
* fail the AI declaration check on a GENESIS_AI_CONSUMES it cannot use GENC-1611 35ab208
|
|
39
|
+
* give each user their own assistant session and check the right on every mount GENC-1611 aedc3e3
|
|
40
|
+
* hold Gemini to one candidate so the token cap is the real cap GENC-1609 f238852
|
|
41
|
+
* keep a bad key and failed calls out of replies and logs GENC-1609 2da7ed6
|
|
42
|
+
* keep the key out of vendor replies and bare model ids in the proxy GENC-1609 1865ad5
|
|
43
|
+
* move the UI packages to 15.45.0 GENC-1611 9f85173
|
|
44
|
+
* put the AI settings in the system definition the server reads GENC-1609 8082946
|
|
45
|
+
* raise the router body cap above the chat proxy's 5 MiB limit GENC-1609 42d8bca
|
|
46
|
+
* read the AI keys from the environment, never the system definition GENC-1609 32d5249
|
|
47
|
+
* refuse a nonsense AI_MAX_OUTPUT_TOKENS instead of guessing, and never clamp below 1 GENC-1609 ae9cadc
|
|
48
|
+
* refuse every model when the allow-list is empty GENC-1609 be10603
|
|
49
|
+
* refuse fallback models instead of vetting them GENC-1609 a32f906
|
|
50
|
+
* refuse Gemini's snake_case spellings of the capped fields GENC-1609 83401f7
|
|
51
|
+
* reply through the router's stateless JSON writer GENC-1609 8bc9148
|
|
52
|
+
* run chat calls outside a database transaction GENC-1609 97784a2
|
|
53
|
+
* say plainly that a sent vendor call runs to completion GENC-1609 7fa161f
|
|
54
|
+
* stop holding a thread for the whole vendor call GENC-1609 6ab3e9b
|
|
55
|
+
* stop logging every chat request with the caller's session GENC-1609 4c1eaf9
|
|
56
|
+
* use inject, not the deprecated injector, in the proxy GENC-1609 f0fae49
|
|
57
|
+
* warn against requiresAuth and make the auth checks exact GENC-1609 399ea9b
|
|
46
58
|
|
|
47
59
|
## [5.27.1](https://github.com/genesiscommunitysuccess/blank-app-seed/compare/v5.27.0...v5.27.1) (2026-09-24)
|
|
48
60
|
|
package/README.md
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
# {{appName}}
|
|
2
2
|
|
|
3
3
|
{{{description}}}
|
|
4
|
+
{{#if AI.enabled}}
|
|
5
|
+
|
|
6
|
+
## AI chat
|
|
7
|
+
|
|
8
|
+
This application includes an AI chat: an assistant in the app, and the endpoint it calls,
|
|
9
|
+
`/gwf/ai-service/<vendor>/chat`, which talks to **your** AI vendor with **your** key. Nothing is
|
|
10
|
+
routed through Genesis.
|
|
11
|
+
|
|
12
|
+
**Set your key where the server runs, never in a tracked file in this project.** The chat endpoint
|
|
13
|
+
reads it from the environment that starts the server, so set the one for your vendor there, then
|
|
14
|
+
restart it (it is read once at boot):
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
AI_ANTHROPIC_API_KEY=sk-ant-...
|
|
18
|
+
AI_GEMINI_API_KEY=...
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Do not write the key into `docker-compose.yml`, which is committed with the project. Pass it through
|
|
22
|
+
from the shell (`environment: [AI_ANTHROPIC_API_KEY]`) or keep it in an untracked `.env` file
|
|
23
|
+
(`env_file: .env`), which the project's `.gitignore` already excludes.
|
|
24
|
+
|
|
25
|
+
**Never set it as a system-definition item.** Genesis turns every `GENESIS_SYSDEF_*` variable into
|
|
26
|
+
one, and writes it in plain text into the files a build or an install renders
|
|
27
|
+
(`server/{{appName}}-app/build/genesis/rendered-templates/generated-system-definition.json`, and
|
|
28
|
+
`generated/cfg/generated-system-definition.json` under the Genesis home, including the one under
|
|
29
|
+
`build/`), into `~/.bashrc` inside the app's container, and into the logs at TRACE. So
|
|
30
|
+
`GENESIS_SYSDEF_AI_ANTHROPIC_API_KEY` and `GENESIS_SYSDEF_AI_GEMINI_API_KEY`, which earlier versions
|
|
31
|
+
of this app read, are refused: the endpoint logs a warning and answers as if no key were set. If you
|
|
32
|
+
set one, remove it, delete the server's `build/` directory and the Genesis home's `generated/cfg`,
|
|
33
|
+
and rotate the key: every copy of those files holds it. `GENESIS_ENCRYPTED_SYSDEF_...` is not read
|
|
34
|
+
either.
|
|
35
|
+
|
|
36
|
+
**Who can use it.** Calling the chat endpoint needs the `AI_CHAT` right. An app generated by Genesis
|
|
37
|
+
Create with AI Chat selected already has it: the `AI_CHAT_USER` profile holds the right, and `admin`
|
|
38
|
+
is in that profile. Add other users to that profile, or grant the right through any other profile.
|
|
39
|
+
Otherwise, create the `AI_CHAT` right and grant it the same way.
|
|
40
|
+
|
|
41
|
+
**The assistant.** Signed-in users get a chat bubble at the bottom right of every page, which opens
|
|
42
|
+
the assistant and can be dragged out of the way. Each user gets a conversation of their own, kept until
|
|
43
|
+
the page is reloaded. A user without the `AI_CHAT` right is told so in a banner when they open it,
|
|
44
|
+
and the assistant sends nothing. Rights are read at sign-in, so sign in again after a change. The
|
|
45
|
+
assistant can read the data this project exposes to it: which resources, and what it is told about
|
|
46
|
+
each, is in `client/src/ai/generated/ai-config.json`. That folder is rewritten every time the project
|
|
47
|
+
is generated, so do not edit it; add your own tools in `client/src/ai/extensions/index.ts`, which
|
|
48
|
+
shows how. The client's `build` and `dev` scripts build it with `GENX_ENABLE_AI=true`; without that
|
|
49
|
+
the banner says AI is switched off. Whenever the assistant is blocked, its message box reads "AI usage
|
|
50
|
+
limit reached", whatever the reason; the banner above it gives the real one.
|
|
51
|
+
|
|
52
|
+
**Bound what it can do.** The limits this app was generated with are built into the chat endpoint,
|
|
53
|
+
`server/{{appName}}-app/src/main/genesis/scripts/ai-service-web-handler.kts`. Override either with a
|
|
54
|
+
system-definition item of the same name, set from the environment as `GENESIS_SYSDEF_<name>` (they
|
|
55
|
+
are not secret):
|
|
56
|
+
|
|
57
|
+
- `AI_ALLOWED_MODELS` (`GENESIS_SYSDEF_AI_ALLOWED_MODELS`) lists the models a request may ask for.
|
|
58
|
+
**Set but empty, it refuses every model.**
|
|
59
|
+
- `AI_MAX_OUTPUT_TOKENS` (`GENESIS_SYSDEF_AI_MAX_OUTPUT_TOKENS`) caps the output a single call can
|
|
60
|
+
request (Gemini is also held to one candidate).
|
|
61
|
+
|
|
62
|
+
Fallback models and streamed replies are refused. Vendor server-side tools (for example Anthropic web
|
|
63
|
+
search or Gemini Google Search grounding) are **not** filtered: anyone with `AI_CHAT` can request
|
|
64
|
+
them, and they are billed to your key.
|
|
65
|
+
|
|
66
|
+
**If the chat cannot answer.** With no key configured the endpoint answers **424** with the code
|
|
67
|
+
`NO_API_KEY` (`BAD_API_KEY` if the key contains characters an API key cannot have). A key the vendor
|
|
68
|
+
rejects, revoked or wrong, comes back as the vendor's own error, usually 401 or 400. The assistant
|
|
69
|
+
tries a failed call three times in all and then shows only a generic error, so check the response in
|
|
70
|
+
your browser's developer tools.
|
|
71
|
+
|
|
72
|
+
**Things to know before deploying.**
|
|
73
|
+
|
|
74
|
+
- For the chat, the server accepts request bodies up to 6 MiB instead of the default 256 KiB. The chat
|
|
75
|
+
endpoint itself takes up to 5 MiB, and the extra room lets it answer a turn just over that with its
|
|
76
|
+
own `REQUEST_TOO_LARGE` code. The 6 MiB limit is router-wide and applies before login, so it covers
|
|
77
|
+
every endpoint, including unauthenticated ones.
|
|
78
|
+
- The app's Docker image puts nginx in front of the server, and its `nginx.conf` sets no
|
|
79
|
+
`client_max_body_size`, so nginx's 1 MiB default applies: raise it to `6m`, or chat turns over
|
|
80
|
+
1 MiB are refused before they reach the server. The Genesis Gradle plugin rewrites that
|
|
81
|
+
`nginx.conf` on every build, so don't edit it. Add a file under `/etc/nginx/conf.d/` containing
|
|
82
|
+
`client_max_body_size 6m;` instead (`nginx.conf` includes `conf.d/*.conf` inside its `http` block),
|
|
83
|
+
for example from a Dockerfile you supply through the plugin's `customDockerfile` setting, which
|
|
84
|
+
replaces the generated one. Any other reverse proxy needs the same limit.
|
|
85
|
+
- The server's default CORS policy accepts any origin with credentials, and the session cookie is
|
|
86
|
+
`SameSite=Lax`. A page on the same site as this app (a sibling subdomain, for example) can therefore
|
|
87
|
+
call the chat as a logged-in user who holds `AI_CHAT`, and spend your key. Serve the app from a
|
|
88
|
+
domain nothing else shares, or restrict CORS, before exposing it.
|
|
89
|
+
{{/if}}
|
|
4
90
|
|
|
5
91
|
{{!
|
|
6
92
|
|
|
@@ -57,6 +143,59 @@ When first opening the project, if you receive a notification from IntelliJ IDE
|
|
|
57
143
|
The Web client for this project can be found [here](./client/README.md). It is built using Genesis's next
|
|
58
144
|
generation web development framework, which is based on Web Components.
|
|
59
145
|
|
|
146
|
+
## Running the application
|
|
147
|
+
|
|
148
|
+
Genesis Start runs the server for you. From `client/`:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
npm run genesis-start # opens the Genesis Start desktop launcher
|
|
152
|
+
npm run genesis-start:headless # the same launcher with no window, driven over a REST API on port 18080
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
On Windows, npm runs these scripts in `cmd.exe`, where `./gradlew` does not work. From `client/`, run
|
|
156
|
+
`cd ..\server && gradlew.bat genesisStart` instead, adding the `-P` flags from `package.json` for
|
|
157
|
+
headless.
|
|
158
|
+
|
|
159
|
+
**Headless** is for a build machine or a remote development environment:
|
|
160
|
+
|
|
161
|
+
- `npm run genesis-start:headless` returns once the launcher is up and prints
|
|
162
|
+
`Genesis Start started with PID <n>`. The launcher keeps running in the background, even after the
|
|
163
|
+
terminal closes, and Ctrl+C does not stop it.
|
|
164
|
+
- To stop it, stop the app's processes first:
|
|
165
|
+
`curl -X POST http://localhost:18080/api/processes/application/stop-all`, then wait until
|
|
166
|
+
`curl http://localhost:18080/api/processes` shows none of them `RUNNING`, and stop any utility still
|
|
167
|
+
running with `curl -X POST http://localhost:18080/api/processes/<id>/stop`. Only then `kill <n>`.
|
|
168
|
+
Stopping the launcher first leaves every process it started running and holding its ports.
|
|
169
|
+
- It takes over every Genesis process already running on the machine when it starts, so stop-all
|
|
170
|
+
stops those too, even another app's.
|
|
171
|
+
- It logs to `server/build/genesis-start/output.log` and `error.log`; if it exits within five seconds,
|
|
172
|
+
Gradle says so and points there. Each process it starts logs under the app's Genesis home, and
|
|
173
|
+
`GET /api/processes/{processId}/log` returns that log.
|
|
174
|
+
- Check it is up with `curl http://localhost:18080/api/health`. The API is described at
|
|
175
|
+
`http://localhost:18080/api/docs`.
|
|
176
|
+
- `npm run genesis-start:write-script` writes `server/build/genesis-start/start.sh` (`start.bat` on
|
|
177
|
+
Windows), which runs the same headless launcher without Gradle, in the foreground, logging to the
|
|
178
|
+
same two files. Ctrl+C stops only the launcher, so stop the processes first, as above.
|
|
179
|
+
- The script, and the command line Gradle prints as it starts the launcher (which `ps` also shows),
|
|
180
|
+
can hold the database password in plain text.
|
|
181
|
+
|
|
182
|
+
**The REST API has no authentication and listens on every network interface.** Anyone who can reach
|
|
183
|
+
port 18080 can:
|
|
184
|
+
|
|
185
|
+
- start and stop the app's processes, and apply schema changes to its database (`/api/bootstrap`
|
|
186
|
+
runs Remap);
|
|
187
|
+
- write rows into any table: `/api/import` loads files from this machine with SendIt, and a file's
|
|
188
|
+
name picks the table, so a `USER.csv` or `PROFILE_USER.csv` can add users or give them profiles,
|
|
189
|
+
and with them rights such as `AI_CHAT`. It takes a path; 0.1.15 refuses an uploaded file, but a
|
|
190
|
+
later version may not;
|
|
191
|
+
- run the app's scripts with any arguments and working directory (`/api/scripts/run`), and type into
|
|
192
|
+
a running utility (`/api/processes/{processId}/input`).
|
|
193
|
+
|
|
194
|
+
Run headless only on a machine and network you trust, or block port 18080 from anything else. A web
|
|
195
|
+
page you visit on that machine may be able to reach `localhost:18080` too: the API checks no origin,
|
|
196
|
+
and it takes JSON sent as plain text, so a page can stop or start processes and run the app's scripts
|
|
197
|
+
without a CORS preflight.
|
|
198
|
+
|
|
60
199
|
# License
|
|
61
200
|
|
|
62
201
|
This is free and unencumbered software released into the public domain. For full terms, see [LICENSE](./LICENSE)
|
|
@@ -6,6 +6,7 @@ plugins {
|
|
|
6
6
|
description = "{{appName}} BDD Testing Framework"
|
|
7
7
|
|
|
8
8
|
repositories {
|
|
9
|
+
mavenCentral()
|
|
9
10
|
maven {
|
|
10
11
|
val repoUrl = if (properties["useDevRepo"] == "true") {
|
|
11
12
|
"https://genesisglobal.jfrog.io/genesisglobal/dev-repo"
|
|
@@ -18,7 +19,6 @@ repositories {
|
|
|
18
19
|
password = properties["genesisArtifactoryPassword"].toString()
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
mavenCentral()
|
|
22
22
|
mavenLocal {
|
|
23
23
|
// VERY IMPORTANT!!! EXCLUDE AGRONA AS IT IS A POM DEPENDENCY AND DOES NOT PLAY NICELY WITH MAVEN LOCAL!
|
|
24
24
|
content {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
distributionBase=GRADLE_USER_HOME
|
|
2
2
|
distributionPath=wrapper/dists
|
|
3
|
-
distributionUrl=https\://services.gradle.org/distributions/gradle-
|
|
3
|
+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
|
4
4
|
networkTimeout=10000
|
|
5
5
|
validateDistributionUrl=true
|
|
6
6
|
zipStoreBase=GRADLE_USER_HOME
|