@frontera-sdk/cli 1.52.39 → 1.52.40

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 (225) hide show
  1. package/README.md +9 -0
  2. package/package.json +6 -4
  3. package/src/adopt.ts +6 -2
  4. package/src/api/apps-api.ts +86 -4
  5. package/src/api/blueprint-authoring-api.ts +35 -11
  6. package/src/api/dataset-api.ts +29 -0
  7. package/src/api/governed-action-api.ts +90 -2
  8. package/src/api/platform-api.ts +87 -2
  9. package/src/api/workflow-api.ts +33 -2
  10. package/src/app-contract.ts +156 -0
  11. package/src/app-lock.ts +125 -0
  12. package/src/args.ts +14 -3
  13. package/src/auth-verify.ts +10 -0
  14. package/src/automation-template.ts +12 -1
  15. package/src/blueprint/action-rules.ts +516 -0
  16. package/src/blueprint/action-semantic-fit.ts +205 -0
  17. package/src/blueprint/compile.ts +51 -2
  18. package/src/blueprint/diff.ts +34 -2
  19. package/src/blueprint/metric-query.ts +215 -0
  20. package/src/blueprint/metric-rules.ts +122 -0
  21. package/src/blueprint/object-sets.ts +116 -0
  22. package/src/blueprint/ontology-edit-plan.ts +48 -4
  23. package/src/blueprint/projection.ts +24 -1
  24. package/src/blueprint/scaffold.ts +84 -42
  25. package/src/commands/action/available.ts +1 -1
  26. package/src/commands/action/criteria-refusal.ts +84 -0
  27. package/src/commands/action/decide.ts +5 -2
  28. package/src/commands/action/deploy.ts +13 -4
  29. package/src/commands/action/list.ts +134 -19
  30. package/src/commands/action/requests.ts +34 -6
  31. package/src/commands/action/submit.ts +28 -13
  32. package/src/commands/agent/index-commands.ts +69 -7
  33. package/src/commands/agent/resolve.ts +2 -0
  34. package/src/commands/app/checks.ts +44 -0
  35. package/src/commands/app/deploy.ts +243 -60
  36. package/src/commands/app/deployments.ts +97 -0
  37. package/src/commands/app/dev.ts +103 -11
  38. package/src/commands/app/init.ts +36 -3
  39. package/src/commands/app/promote.ts +45 -19
  40. package/src/commands/app/pull.ts +3 -1
  41. package/src/commands/app/rollback.ts +46 -0
  42. package/src/commands/app/sdk.ts +54 -8
  43. package/src/commands/app/unpin.ts +27 -0
  44. package/src/commands/app/versions.ts +1 -1
  45. package/src/commands/automation/bindings.ts +4 -2
  46. package/src/commands/automation/build-entry.ts +4 -1
  47. package/src/commands/automation/index-commands.ts +3 -1
  48. package/src/commands/automation/pull.ts +6 -1
  49. package/src/commands/automation/run.ts +34 -9
  50. package/src/commands/blueprint/bind.ts +2 -1
  51. package/src/commands/blueprint/declarative.ts +331 -31
  52. package/src/commands/blueprint/get.ts +6 -2
  53. package/src/commands/blueprint/grants.ts +64 -10
  54. package/src/commands/blueprint/metric.ts +108 -0
  55. package/src/commands/blueprint/object-sets.ts +71 -0
  56. package/src/commands/blueprint/query.ts +155 -31
  57. package/src/commands/dataset/index-commands.ts +156 -7
  58. package/src/commands/knowledge/index-commands.ts +122 -20
  59. package/src/commands/local/check.ts +657 -44
  60. package/src/commands/local/data.ts +33 -12
  61. package/src/commands/local/dev.ts +875 -88
  62. package/src/commands/local/docs-mcp.ts +10 -18
  63. package/src/commands/local/index-commands.ts +300 -63
  64. package/src/commands/local/init.ts +18 -5
  65. package/src/commands/local/key.ts +0 -3
  66. package/src/commands/local/providers.ts +0 -7
  67. package/src/commands/local/remove.ts +116 -0
  68. package/src/commands/local/schema.ts +3 -4
  69. package/src/commands/local/sdk-refresh.ts +59 -0
  70. package/src/commands/local/snapshot.ts +100 -0
  71. package/src/commands/local/sync-request.ts +320 -13
  72. package/src/commands/local/sync.ts +102 -5
  73. package/src/commands/mcp/serve.ts +34 -0
  74. package/src/commands/registry.ts +52 -8
  75. package/src/commands/types.ts +29 -0
  76. package/src/commands/workflow/archive.ts +46 -0
  77. package/src/commands/workflow/get.ts +2 -1
  78. package/src/commands/workflow/index-commands.ts +5 -0
  79. package/src/commands/workflow/push.ts +49 -2
  80. package/src/commands/workflow/run.ts +65 -7
  81. package/src/commands/workflow/runs.ts +9 -6
  82. package/src/commands/workflow/webhook-test.ts +166 -0
  83. package/src/config.ts +43 -6
  84. package/src/dev-broker.ts +52 -0
  85. package/src/exit.ts +103 -4
  86. package/src/flag-help.ts +77 -17
  87. package/src/forge/touches.ts +15 -12
  88. package/src/function-outside-imports.ts +89 -0
  89. package/src/harness.ts +42 -24
  90. package/src/heal.ts +112 -1
  91. package/src/local/fix-words.ts +12 -0
  92. package/src/local/function-bundle-recovery.ts +775 -0
  93. package/src/local/local-actions.ts +103 -10
  94. package/src/local/local-agent-action-grants.ts +210 -0
  95. package/src/local/local-agent-checks.ts +232 -0
  96. package/src/local/local-agent-grants.ts +29 -7
  97. package/src/local/local-agent-lean-plugins.ts +68 -0
  98. package/src/local/local-agent-missing-tools.ts +68 -0
  99. package/src/local/local-agent-name.ts +27 -0
  100. package/src/local/local-app-code.ts +191 -0
  101. package/src/local/local-app-dev-files.ts +186 -0
  102. package/src/local/local-app-dev-session.ts +88 -0
  103. package/src/local/local-app-lock-install.ts +114 -0
  104. package/src/local/local-app-pulled.ts +167 -0
  105. package/src/local/local-app-restore.ts +111 -0
  106. package/src/local/local-app-runtime.ts +68 -0
  107. package/src/local/local-app-sdk-paths.ts +149 -0
  108. package/src/local/local-app-sdk.ts +509 -0
  109. package/src/local/local-applied.ts +211 -21
  110. package/src/local/local-apply-history.ts +84 -0
  111. package/src/local/local-apply-summary.ts +86 -0
  112. package/src/local/local-apply-watchdog.ts +95 -0
  113. package/src/local/local-apps.ts +180 -7
  114. package/src/local/local-blueprint-errors.ts +198 -0
  115. package/src/local/local-blueprint-outline.ts +31 -12
  116. package/src/local/local-blueprint-schemas.ts +582 -0
  117. package/src/local/local-build-inputs.ts +48 -0
  118. package/src/local/local-build-summary.ts +495 -28
  119. package/src/local/local-case-evidence.ts +120 -0
  120. package/src/local/local-case-not-run.ts +153 -0
  121. package/src/local/local-chat-apps.ts +93 -16
  122. package/src/local/local-check-inherited.ts +83 -0
  123. package/src/local/local-cities.ts +89 -0
  124. package/src/local/local-component-health.ts +119 -0
  125. package/src/local/local-control.ts +34 -7
  126. package/src/local/local-db-snapshot.ts +348 -0
  127. package/src/local/local-dev-references.ts +346 -35
  128. package/src/local/local-dev-sync.ts +136 -7
  129. package/src/local/local-docker.ts +35 -1
  130. package/src/local/local-docs-search.ts +1 -1
  131. package/src/local/local-docs.ts +281 -2
  132. package/src/local/local-empty-workflows.ts +92 -0
  133. package/src/local/local-env.ts +16 -1
  134. package/src/local/local-file-checks.ts +104 -0
  135. package/src/local/local-function-ids.ts +141 -0
  136. package/src/local/local-function-recovery.ts +141 -0
  137. package/src/local/local-function-state.ts +87 -0
  138. package/src/local/local-keys.ts +7 -5
  139. package/src/local/local-knowledge-ingestion.ts +225 -0
  140. package/src/local/local-knowledge-uploads.ts +217 -0
  141. package/src/local/local-media-placeholders.ts +182 -0
  142. package/src/local/local-media-sets.ts +90 -10
  143. package/src/local/local-model-parameters.ts +89 -0
  144. package/src/local/local-object-sets.ts +18 -6
  145. package/src/local/local-operating.ts +35 -1
  146. package/src/local/local-org-settings.ts +205 -7
  147. package/src/local/local-orphans.ts +321 -0
  148. package/src/local/local-personas.ts +296 -0
  149. package/src/local/local-portability.ts +141 -16
  150. package/src/local/local-ports.ts +99 -7
  151. package/src/local/local-project-notices.ts +234 -0
  152. package/src/local/local-project-secrets.ts +133 -0
  153. package/src/local/local-project.ts +12 -0
  154. package/src/local/local-provider-refusal.ts +66 -0
  155. package/src/local/local-request-evidence.ts +112 -0
  156. package/src/local/local-runtime-health.ts +123 -0
  157. package/src/local/local-runtime-history.ts +50 -0
  158. package/src/local/local-runtime-target.ts +42 -0
  159. package/src/local/local-sample-data.ts +141 -0
  160. package/src/local/local-sample-remake.ts +138 -0
  161. package/src/local/local-scaffold.ts +336 -53
  162. package/src/local/local-schema-check.ts +148 -0
  163. package/src/local/local-schemas.ts +742 -25
  164. package/src/local/local-secret-carry.ts +107 -0
  165. package/src/local/local-secrets.ts +38 -8
  166. package/src/local/local-seed.ts +102 -26
  167. package/src/local/local-stand-ins.ts +1 -1
  168. package/src/local/local-state.ts +43 -2
  169. package/src/local/local-studio-server.ts +170 -7
  170. package/src/local/local-supervisor.ts +44 -6
  171. package/src/local/local-sync-names.ts +2 -0
  172. package/src/local/local-sync-plan.ts +11 -0
  173. package/src/local/local-sync-push.ts +10 -7
  174. package/src/local/local-sync-refs.ts +49 -3
  175. package/src/local/local-sync-remote.ts +343 -32
  176. package/src/local/local-sync-snapshot.ts +15 -10
  177. package/src/local/local-synthetic-data.ts +2116 -99
  178. package/src/local/local-test-baseline.ts +113 -0
  179. package/src/local/local-test-cases.ts +76 -1
  180. package/src/local/local-test-runner.ts +582 -91
  181. package/src/local/local-test-runs.ts +13 -5
  182. package/src/local/local-vendored-sdk.ts +183 -0
  183. package/src/local/local-workflow-state.ts +16 -3
  184. package/src/local/local-workflow-tests.ts +482 -0
  185. package/src/local/local-workspace-grants.ts +81 -0
  186. package/src/main.ts +50 -9
  187. package/src/mcp/a11y-scan.ts +55 -0
  188. package/src/mcp/browser/ax-snapshot.ts +181 -0
  189. package/src/mcp/browser/credentials.ts +78 -0
  190. package/src/mcp/browser/frames.ts +336 -0
  191. package/src/mcp/browser/headless-session.ts +413 -0
  192. package/src/mcp/browser/headless-tab.ts +575 -0
  193. package/src/mcp/browser/keys.ts +70 -0
  194. package/src/mcp/browser/page-scripts.ts +129 -0
  195. package/src/mcp/browser/wait.ts +73 -0
  196. package/src/mcp/forge-link.ts +126 -0
  197. package/src/mcp/headless.ts +199 -0
  198. package/src/mcp/mcp-apps.ts +435 -0
  199. package/src/mcp/mcp-chat.ts +226 -0
  200. package/src/mcp/mcp-cli.ts +53 -0
  201. package/src/mcp/mcp-protocol.ts +122 -0
  202. package/src/mcp/mcp-runtime.ts +113 -0
  203. package/src/mcp/mcp-server.ts +108 -0
  204. package/src/mcp/mcp-traces.ts +409 -0
  205. package/src/mcp/page-observation.ts +193 -0
  206. package/src/mcp/tools/agent-tools.ts +700 -0
  207. package/src/mcp/tools/app-tools.ts +241 -0
  208. package/src/mcp/tools/browser-tools.ts +310 -0
  209. package/src/mcp/tools/docs-tools.ts +32 -0
  210. package/src/mcp/tools/project-tools.ts +205 -0
  211. package/src/mcp/tools/test-tools.ts +339 -0
  212. package/src/mcp/tools/workflow-tools.ts +611 -0
  213. package/src/output.ts +1 -1
  214. package/src/pack.ts +8 -3
  215. package/src/packaging.ts +1 -1
  216. package/src/project-bootstrap.ts +107 -4
  217. package/src/project-context.ts +18 -1
  218. package/src/project.ts +27 -2
  219. package/src/provenance.ts +25 -0
  220. package/src/render-evidence.ts +65 -7
  221. package/src/template.ts +14 -8
  222. package/src/templates/next-app-files.ts +35 -6
  223. package/src/templates/next-skills.ts +61 -27
  224. package/src/vendor/kit-assets.json +6 -6
  225. package/src/vendor/sdk-sources.json +24 -21
@@ -2,8 +2,11 @@ import { existsSync, readFileSync, rmSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
 
4
4
  import { writeFileAtomic } from '../fs-atomic'
5
+ import { BLUEPRINT_SCHEMA_FILES, BLUEPRINT_SCHEMAS } from './local-blueprint-schemas'
5
6
  import type { LocalProject } from './local-project'
6
7
  import { isAlive, readState } from './local-state'
8
+ import { PROCEDURE_TOOL_FORMS, PROCEDURE_TOOL_GROUPS } from './local-agent-checks'
9
+ import { REF_KINDS, refPattern, type RefKind } from './local-sync-refs'
7
10
 
8
11
  /**
9
12
  * The formats of a project's files, as JSON Schema, for the coding agents and
@@ -11,13 +14,37 @@ import { isAlive, readState } from './local-state'
11
14
  *
12
15
  * The service checks three of them, so their schemas come from the service
13
16
  * the runtime runs (`GET /v1/config/file-schemas/<kind>`), made from the same
14
- * zod schemas that check the files. The other two only this CLI reads, so it
15
- * describes them itself, beside the code that reads them. `local start` writes
16
- * all five into `.frontera/local/schemas/` once the service is up, so they can
17
- * be read while the runtime is stopped and always describe the release it ran.
17
+ * zod schemas that check the files, then widened where a project file names a
18
+ * resource by reference instead of id (`localizeServiceSchema`). The others
19
+ * (Agent and workflow tests, datasets, the Blueprint's files) only this CLI
20
+ * reads, so it describes them itself, beside the code that reads them. `local
21
+ * start` writes all of them into `.frontera/local/schemas/` once the service is
22
+ * up, so they can be read while the runtime is stopped and always describe the
23
+ * release it ran.
18
24
  */
19
25
 
20
- export const SCHEMA_KINDS = ['operating', 'agent', 'workflow', 'evals', 'dataset'] as const
26
+ export const SCHEMA_KINDS = [
27
+ 'operating',
28
+ 'agent',
29
+ 'workflow',
30
+ 'evals',
31
+ 'workflow-tests',
32
+ 'dataset',
33
+ 'bindings',
34
+ 'theme',
35
+ 'chat-app',
36
+ 'knowledge',
37
+ 'skill',
38
+ 'blueprint-object-type',
39
+ 'blueprint-link-type',
40
+ 'blueprint-shared-field',
41
+ 'blueprint-semantic-type',
42
+ 'blueprint-metric',
43
+ 'blueprint-action',
44
+ 'blueprint-object-set',
45
+ 'blueprint-workspace-grants',
46
+ 'secrets',
47
+ ] as const
21
48
  export type SchemaKind = (typeof SCHEMA_KINDS)[number]
22
49
 
23
50
  /** Where each kind of file lives in a project. */
@@ -26,7 +53,15 @@ export const SCHEMA_FILES: Record<SchemaKind, string> = {
26
53
  agent: 'agents/<slug>/agent.json',
27
54
  workflow: 'workflows/<slug>.yaml',
28
55
  evals: 'agents/<slug>/evals/*.yaml',
56
+ 'workflow-tests': 'workflows/tests/<slug>.yaml',
29
57
  dataset: 'datasets/<apiName>.json',
58
+ bindings: 'agents/<slug>/bindings.json',
59
+ theme: 'themes/<name>/theme.json',
60
+ 'chat-app': 'chat-apps/<slug>.json',
61
+ knowledge: 'knowledge/<name>/knowledge.json',
62
+ skill: 'skills/<name>/SKILL.md (its front matter)',
63
+ secrets: 'secrets.json',
64
+ ...BLUEPRINT_SCHEMA_FILES,
30
65
  }
31
66
 
32
67
  export function isSchemaKind(value: string): value is SchemaKind {
@@ -37,6 +72,51 @@ const DRAFT = 'https://json-schema.org/draft/2020-12/schema'
37
72
 
38
73
  type JsonSchema = Record<string, unknown>
39
74
 
75
+ /**
76
+ * One check on a case's last reply, by its `type` (`runCheck` in `local-test-runner.ts`).
77
+ * Open to other keys: a check pulled from the platform may carry the platform's own.
78
+ */
79
+ function CHECK(type: 'contains' | 'regex' | 'tool_called' | 'tool_not_called'): JsonSchema {
80
+ const text = { type: 'string', description: 'What the check means, in words; shown when it fails.' }
81
+ if (type === 'contains') {
82
+ return {
83
+ title: 'contains',
84
+ type: 'object',
85
+ properties: { type: { const: 'contains' }, value: { type: 'string', minLength: 1, description: 'The phrase the reply must hold, ignoring case. Text: quote a number (`"12"`).' }, text },
86
+ required: ['type', 'value'],
87
+ }
88
+ }
89
+ if (type === 'regex') {
90
+ return {
91
+ title: 'regex',
92
+ type: 'object',
93
+ properties: {
94
+ type: { const: 'regex' },
95
+ value: { type: 'string', minLength: 1, maxLength: 500, description: 'The pattern the reply must match (JavaScript syntax); a nested quantifier such as `(a+)+` is refused.' },
96
+ flags: { type: 'string', pattern: '^[dgimsuvy]*$', description: 'Regex flags; `i` when absent.' },
97
+ text,
98
+ },
99
+ required: ['type', 'value'],
100
+ }
101
+ }
102
+ return {
103
+ title: type,
104
+ type: 'object',
105
+ properties: {
106
+ type: { const: type },
107
+ tool: { type: 'string', minLength: 1, description: 'The tool’s name as the trace shows it (`function_<slug>` for a Function, `searchKnowledge`; a Plugin’s tool by its capability name, `mcp__…`, never its bare name).' },
108
+ argsSubset: { type: 'object', description: 'The arguments the call must carry.' },
109
+ turn: { enum: ['all', 'last'], description: 'Which turns’ calls the check reads: every turn of the case (`all`, the default) or only the last one, the turn whose reply the case grades.' },
110
+ mode: { enum: ['exact', 'ignore', 'subset', 'superset'], description: 'How argsSubset is compared; superset when absent. The `action` narration an Agent may add to any call is left out of the comparison unless argsSubset names it.' },
111
+ text,
112
+ },
113
+ required: ['type', 'tool'],
114
+ }
115
+ }
116
+
117
+ /** The longest wait before one turn of an Agent case (`waitSeconds` in the evals format). */
118
+ export const MAX_TURN_WAIT_SECONDS = 300
119
+
40
120
  /** `agents/<slug>/evals/*.yaml`, as `local test` and Studio read it (`local-test-runner.ts`). */
41
121
  const EVALS_SCHEMA: JsonSchema = {
42
122
  $schema: DRAFT,
@@ -54,13 +134,27 @@ const EVALS_SCHEMA: JsonSchema = {
54
134
  conversation: {
55
135
  type: 'array',
56
136
  minItems: 1,
57
- description: 'One turn per item. Only the user turns are sent; an assistant turn is kept for whoever reads the case.',
137
+ description: 'One turn per item. Only the user turns are sent, in order, each as soon as the reply before it ends; an assistant turn is kept for whoever reads the case. `waitSeconds` on a user turn keeps a gap before it (`- { user: "Still there?", waitSeconds: 60 }` sends it a minute after the reply before).',
58
138
  items: {
59
139
  type: 'object',
60
- properties: { user: { type: 'string' }, assistant: { type: 'string' } },
140
+ properties: {
141
+ user: { type: 'string', description: 'What the person says; sent.' },
142
+ assistant: { type: 'string', description: 'A reply, kept for whoever reads the case; not sent and not checked.' },
143
+ waitSeconds: {
144
+ type: 'integer',
145
+ minimum: 1,
146
+ maximum: MAX_TURN_WAIT_SECONDS,
147
+ description: `With \`user\`: how long to wait, in seconds, before sending this turn (at most ${MAX_TURN_WAIT_SECONDS}), so the case keeps a gap such as "turn 2 a minute after turn 1". The case takes that much longer, and its result says it waited (\`waited 60 s before turn 2\`).`,
148
+ },
149
+ },
61
150
  minProperties: 1,
62
- maxProperties: 1,
151
+ maxProperties: 2,
63
152
  additionalProperties: false,
153
+ // A turn is one person's message (with its wait) or one reply, never both.
154
+ anyOf: [
155
+ { properties: { user: { type: 'string' }, waitSeconds: { type: 'integer' } }, required: ['user'], additionalProperties: false },
156
+ { properties: { assistant: { type: 'string' } }, required: ['assistant'], additionalProperties: false },
157
+ ],
64
158
  },
65
159
  },
66
160
  expected: { type: 'string', description: 'The last reply, word for word; differences in whitespace are ignored.' },
@@ -72,23 +166,31 @@ const EVALS_SCHEMA: JsonSchema = {
72
166
  },
73
167
  checks: {
74
168
  type: 'array',
75
- description: 'Checks on the last reply and the tools the Agent called, graded as the platform grades them; every one must pass.',
76
- items: {
77
- type: 'object',
78
- properties: {
79
- type: { enum: ['contains', 'regex', 'tool_called', 'tool_not_called'] },
80
- value: { type: 'string', description: 'contains: the phrase; regex: the pattern.' },
81
- flags: { type: 'string', description: 'regex flags; i when absent.' },
82
- tool: { type: 'string', description: 'tool_called, tool_not_called: the tool’s name.' },
83
- argsSubset: { type: 'object', description: 'The arguments the call must carry.' },
84
- mode: { enum: ['exact', 'ignore', 'subset', 'superset'], description: 'How argsSubset is compared; superset when absent.' },
85
- text: { type: 'string', description: 'What the check means, in words.' },
86
- },
87
- required: ['type'],
88
- },
169
+ description: 'Checks on the last reply and the tools the Agent called, graded as the platform grades them; every one must pass. `type` says which keys a check takes.',
170
+ items: { oneOf: [CHECK('contains'), CHECK('regex'), CHECK('tool_called'), CHECK('tool_not_called')] },
89
171
  },
90
172
  judged: { type: 'array', items: { type: 'string' }, description: 'Expectations only the platform’s judge grades; kept, not graded locally.' },
91
173
  gold: { type: 'boolean', description: 'Human-certified on the platform.' },
174
+ decision: {
175
+ enum: ['approve', 'deny', 'expect'],
176
+ description:
177
+ 'What the case does when a turn stops on a decision a person makes (a tool that needs approval, a confirmation): approve or deny a tool approval and read the reply on, or expect it (the case passes by waiting, its checks run on the reply so far, and the tool call waiting on the approval counts for `tool_called`, with the arguments it was called with: test that the Agent asks before it acts without approving anything). Without it, a turn that waits fails the case with "waited for a decision: <tool>". A confirmation card cannot be answered here. A case with a decision runs real actions (see realActions): it changes records on the local runtime.',
178
+ },
179
+ realActions: {
180
+ type: 'boolean',
181
+ description:
182
+ 'Run the tools that change something for real, as `agent_chat` with realActions does: the case changes records on the local runtime (a governed Action is submitted, a Function or workflow runs). Without it, and without `decision`, they are simulated: each answers as a success with an empty result and none asks for approval, even one whose binding requires it; only a case with real actions stops on an approval. A case with `decision` (approve, deny or expect) runs real actions whether or not it says so, since only a real action waits for approval; with `expect` the tool waiting on the approval never runs (the case ends there), though tools the Agent called before it did.',
183
+ },
184
+ variables: {
185
+ type: 'object',
186
+ additionalProperties: { type: ['string', 'number', 'boolean'] },
187
+ description: 'The conversation’s variables for this case, by name (`customer_id: CST-00060`), as a channel would set them: what the instructions read as {{customer_id}} and what operating.json `dataScopes` narrow reads by. Without it the case’s conversation has none. Not for a scenario case: the evaluation runner gives a scenario no variables yet.',
188
+ },
189
+ gate: {
190
+ type: 'boolean',
191
+ description:
192
+ "Guards the Agent's behavior: a change request runs the Agent's `gate: true` cases before it applies, against the last applied run, and the workspace can require them to pass. `test_run` with `gate: true` (the MCP server) runs only these.",
193
+ },
92
194
  scenario: {
93
195
  type: 'object',
94
196
  description: 'A conversation with a simulated customer (persona, goal, opening, maxTurns, verified, results, note), run on the local runtime’s evaluation runner.',
@@ -113,6 +215,63 @@ const EVALS_SCHEMA: JsonSchema = {
113
215
  additionalProperties: false,
114
216
  }
115
217
 
218
+ /** One output check of a workflow test case (`local-workflow-tests.ts`). */
219
+ const WORKFLOW_OUTPUT_CHECK = {
220
+ type: 'object',
221
+ properties: {
222
+ path: {
223
+ type: 'string',
224
+ minLength: 1,
225
+ description: 'A dot path into what the run returned (`decision`, `items.0.id`). A leading `output.` also names what it returned (`output.decision` reads `decision`), except when what it returned has a key named `output` (a workflow whose `output` is a Function step’s whole result, `{ runId, output: { … } }`): then the path reads that key, so `output.withdrawn` reads `{ "output": { "withdrawn": true } }`. `steps.<stepId>.output.<path>` reads one step’s output, `steps.<stepId>.status` its status. A forEach step’s output is the list of its iterations, as `$steps` gives it; each iteration holds every inner step’s output under the inner step’s id (`{ "<innerStepId>": { …its output } }`), so read one with `steps.<stepId>.output.0.<innerStepId>.<path>` (for example `steps.escalate_each.output.0.escalate.lifecycle`), and the count with `steps.<stepId>.output.length` (the run’s own view, `.results.0…`, `.count`, reads too).',
226
+ },
227
+ equals: { description: 'The value there, exactly (types count: 120 is not "120").' },
228
+ contains: { description: 'Text there holds this (ignoring case); a list there has an item equal to it; a mapping there holds these keys and values.' },
229
+ exists: { type: 'boolean', description: 'Something is there (true) or nothing is (false).' },
230
+ },
231
+ required: ['path'],
232
+ anyOf: [{ required: ['equals'] }, { required: ['contains'] }, { required: ['exists'] }],
233
+ additionalProperties: false,
234
+ }
235
+
236
+ /** `workflows/tests/<slug>.yaml`, as `local test` and `local check` read it (`local-workflow-tests.ts`). */
237
+ const WORKFLOW_TESTS_SCHEMA: JsonSchema = {
238
+ $schema: DRAFT,
239
+ title: 'Workflow test cases (workflows/tests/<slug>.yaml)',
240
+ description:
241
+ "`frontera local test` (`--workflow <slug>` for one) and `frontera local check` run each case on exactly the version `local dev` pushed workflows/<slug>.yaml as (a test run while it is a draft; a run of the live version once an Agent's binding promoted it): the input (and subject) sent, the run waited for, then its status, failed step and output checked. When the file changed since it was applied, or its apply failed, the cases fail with what to run instead of testing other code.",
242
+ type: 'object',
243
+ properties: {
244
+ workflow: { type: 'string', minLength: 1, description: 'The workflow’s slug (workflows/<slug>.yaml); the file’s name when absent.' },
245
+ cases: {
246
+ type: 'array',
247
+ minItems: 1,
248
+ items: {
249
+ type: 'object',
250
+ properties: {
251
+ name: { type: 'string', minLength: 1, description: 'Names the case in results.' },
252
+ input: { type: 'object', description: 'The run’s input, as the workflow’s `input` declares it.' },
253
+ subject: { type: 'string', minLength: 1, description: 'The primary key of the record a record-bound workflow runs on.' },
254
+ timeoutSeconds: { type: 'integer', minimum: 1, maximum: 900, description: 'How long to wait for the run; 120 when absent.' },
255
+ expect: {
256
+ type: 'object',
257
+ properties: {
258
+ status: { enum: ['succeeded', 'failed', 'canceled'], description: 'How the run must end; succeeded when absent (failed when failedStep or failedCode is set).' },
259
+ failedStep: { type: 'string', minLength: 1, description: 'The id of the step that must fail; a `stop` step with `status: failed` counts as the step that failed its run.' },
260
+ failedCode: { type: 'string', minLength: 1, description: 'The error code a failed step (or the run) must carry, such as CONFIRM_UNAVAILABLE for a confirm step with `onUnavailable: fail`.' },
261
+ output: { type: 'array', items: WORKFLOW_OUTPUT_CHECK, description: 'Checks on what the run returned; every one must pass.' },
262
+ },
263
+ additionalProperties: false,
264
+ },
265
+ },
266
+ required: ['name'],
267
+ additionalProperties: false,
268
+ },
269
+ },
270
+ },
271
+ required: ['cases'],
272
+ additionalProperties: false,
273
+ }
274
+
116
275
  /** The column types `dataset create` accepts (`commands/dataset/index-commands.ts`). */
117
276
  const COLUMN_TYPES = ['text', 'numeric', 'boolean', 'date', 'timestamptz']
118
277
 
@@ -187,6 +346,23 @@ const DATASET_SCHEMA: JsonSchema = {
187
346
  kind: { const: 'file' },
188
347
  path: { type: 'string', minLength: 1, description: 'The CSV, relative to this file.' },
189
348
  includedColumns: COLUMN_NAMES,
349
+ columns: {
350
+ type: 'array',
351
+ minItems: 1,
352
+ description:
353
+ "What the file declares of a column instead of taking it from the first CSV: its nullability, its type, or both (the rest come from the CSV). The first load fixes the dataset's column contract, and a later CSV must fit it, so declare `nullable: true` on every column that may be empty in any CSV you will load (an optional field, a time set only later in a record's life). With `columns`, the CSV holds exactly the included columns. When a later CSV has empty values a column did not allow, `frontera local dev` cuts a new revision of the dataset that allows them and binds its object types to it; a column added, dropped or retyped is a new dataset (another apiName, or `frontera local reset`).",
354
+ items: {
355
+ type: 'object',
356
+ properties: {
357
+ name: { type: 'string', minLength: 1, description: 'One of includedColumns.' },
358
+ nullable: { type: 'boolean' },
359
+ databaseType: { type: 'string', enum: COLUMN_TYPES },
360
+ },
361
+ required: ['name'],
362
+ anyOf: [{ required: ['nullable'] }, { required: ['databaseType'] }],
363
+ additionalProperties: false,
364
+ },
365
+ },
190
366
  },
191
367
  required: ['apiName', 'kind', 'path', 'includedColumns'],
192
368
  additionalProperties: false,
@@ -208,13 +384,553 @@ const DATASET_SCHEMA: JsonSchema = {
208
384
  ],
209
385
  }
210
386
 
387
+ /** `agents/<slug>/bindings.json`, as `local dev` applies it (`applyBindings` in `local-dev-references.ts`). */
388
+ const BINDINGS_SCHEMA: JsonSchema = {
389
+ $schema: DRAFT,
390
+ title: 'What an Agent may use (agents/<slug>/bindings.json)',
391
+ description:
392
+ "The Functions and workflows the Agent may start, and the Blueprint object types it may read (its Context and data). `frontera local dev` applies it after the Agent, its Functions, workflows and the Blueprint: each Function and workflow is bound to the Agent with these settings, and the object types are granted to it (a grant this file does not list is withdrawn). A procedure's `tools.ids` and actions may only name what is bound here, and without `blueprint` the Agent has neither the Blueprint query tools (`tool:blueprint`) nor the governed Action tools (`tool:blueprint-actions`). None of this belongs in agent.json.",
393
+ type: 'object',
394
+ properties: {
395
+ functions: {
396
+ type: 'array',
397
+ description: 'The Functions the Agent may call, as its tools (`function_<slug>` in a trace).',
398
+ items: {
399
+ type: 'object',
400
+ properties: {
401
+ function: { type: 'string', pattern: refPattern(['function']), description: '`function/<slug>`: functions/<slug>.ts, or functions/<slug>/.' },
402
+ enabled: { type: 'boolean', description: 'true when absent.' },
403
+ requiresApproval: { type: 'boolean', description: 'A person approves each call before it runs; true when absent. false lets the Agent call it on its own.' },
404
+ waitSeconds: { type: 'integer', minimum: 5, maximum: 300, description: 'How long the Agent waits for the run before it answers; 90 when absent.' },
405
+ },
406
+ required: ['function'],
407
+ additionalProperties: false,
408
+ },
409
+ },
410
+ workflows: {
411
+ type: 'array',
412
+ description: 'The workflows the Agent may start or read. An Agent runs a workflow’s live version, so `local dev` promotes the version it pushed the workflow’s file as when one bound here has no live version, and after that promotes each new push of it, so the live version stays what the file says.',
413
+ items: {
414
+ type: 'object',
415
+ properties: {
416
+ workflow: { type: 'string', pattern: refPattern(['workflow']), description: '`workflow/<slug>`: workflows/<slug>.yaml.' },
417
+ access: { enum: ['run', 'read'], description: 'run (the default): the Agent may start runs; read: it may only read them.' },
418
+ enabled: { type: 'boolean', description: 'true when absent.' },
419
+ requiresApproval: { type: 'boolean', description: 'A person approves each run before it starts.' },
420
+ waitSeconds: { type: 'integer', minimum: 5, maximum: 300, description: 'How long the Agent waits for the run before it answers; 90 when absent.' },
421
+ },
422
+ required: ['workflow'],
423
+ additionalProperties: false,
424
+ },
425
+ },
426
+ blueprint: {
427
+ type: 'array',
428
+ description: 'Its Context and data: the object types it may query through its Blueprint tools.',
429
+ items: {
430
+ type: 'object',
431
+ properties: {
432
+ objectType: { type: 'string', pattern: refPattern(['objectType']), description: '`objectType/<ApiName>`: blueprint/object-types/<ApiName>.yaml.' },
433
+ permissions: {
434
+ type: 'array',
435
+ items: { enum: ['read', 'read_sensitive', 'act'] },
436
+ description: 'The grant’s whole list. read (always included) sees the records; read_sensitive also sees their confidential and restricted fields; act lets the Agent submit governed Actions on the type (Submit Actions; it implies read). Without act on a type the Agent is offered no Action on it, and the person it works for still needs the Action’s capability. Leaving a permission out takes it away, here and when a change request is applied, so removing act from a pulled list turns Submit Actions off. An entry without permissions is read plus what the Agent’s grant already holds (act and read_sensitive kept).',
437
+ },
438
+ },
439
+ required: ['objectType'],
440
+ additionalProperties: false,
441
+ },
442
+ },
443
+ blueprintUnread: { const: true, description: 'Written by a pull that could not read the Agent’s grants: locally it reads every object type the project holds. Not written by hand.' },
444
+ },
445
+ additionalProperties: false,
446
+ }
447
+
448
+ /** The colors and type of a theme (`SlideThemeTokens` in the service). */
449
+ const HEX = { type: 'string', pattern: '^#[0-9A-Fa-f]{6}$' }
450
+
451
+ /** `themes/<name>/theme.json`, as `local dev` saves it (`applyTheme` in `local-themes.ts`, `POST /v1/slide-themes`). */
452
+ const THEME_SCHEMA: JsonSchema = {
453
+ $schema: DRAFT,
454
+ title: 'Slide theme (themes/<name>/theme.json)',
455
+ description:
456
+ "A slide theme an Agent's decks and images are made in (`createSlides`, `createImage`): its colors and fonts, the style the model follows, and optional layouts and images. The file is `themes/<name>/theme.json`, with every image it names beside it in `themes/<name>/`; `frontera local dev` saves it on the runtime, replacing the copy of the same name. A file elsewhere in themes/ is not applied.",
457
+ type: 'object',
458
+ properties: {
459
+ name: { type: 'string', minLength: 1, maxLength: 200, description: 'How the theme is listed and named in a request.' },
460
+ styleDescription: { type: 'string', maxLength: 20_000, description: 'The look in words; the model reads this most. Say the mood, the layout habits, what to avoid.' },
461
+ source: { enum: ['custom', 'imported'], description: 'custom for one written by hand; imported for one made from a deck.' },
462
+ tokens: {
463
+ type: 'object',
464
+ description: 'The palette and type the slides are built from.',
465
+ properties: {
466
+ mode: { enum: ['light', 'dark'] },
467
+ bg: { ...HEX, description: 'The slide background.' },
468
+ ink: { ...HEX, description: 'The text.' },
469
+ accent: { ...HEX, description: 'The one accent: bars, highlights.' },
470
+ accent2: { ...HEX, description: 'A second accent for charts.' },
471
+ neutral: { ...HEX, description: 'Cards, panels and dividers.' },
472
+ fontHeading: { type: 'string', minLength: 1, description: 'A web-safe font stack, so a .pptx export keeps it ("Arial, Helvetica, sans-serif").' },
473
+ fontBody: { type: 'string', minLength: 1, description: 'A web-safe font stack.' },
474
+ headingWeight: { type: 'integer', minimum: 100, maximum: 900 },
475
+ radius: { type: 'string', description: 'Card corner radius: "12px", or "0" for a flat look.' },
476
+ },
477
+ required: ['mode', 'bg', 'ink', 'accent', 'neutral', 'fontHeading', 'fontBody', 'headingWeight', 'radius'],
478
+ },
479
+ slideCount: { type: 'integer', minimum: 0 },
480
+ layouts: {
481
+ type: 'array',
482
+ maxItems: 200,
483
+ description: 'Page shapes the model maps content onto, each with an optional reference image.',
484
+ items: {
485
+ type: 'object',
486
+ properties: {
487
+ kind: { enum: ['cover', 'section', 'content', 'kpi', 'chart', 'quote', 'grid', 'closing'] },
488
+ hint: { type: 'string', minLength: 1, description: 'The layout in one line.' },
489
+ image: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$', description: 'A file beside theme.json.' },
490
+ },
491
+ required: ['kind', 'hint'],
492
+ additionalProperties: false,
493
+ },
494
+ },
495
+ assets: {
496
+ type: 'array',
497
+ maxItems: 200,
498
+ description: 'Reusable images (backgrounds, logos, art), each a file beside theme.json.',
499
+ items: {
500
+ type: 'object',
501
+ properties: {
502
+ name: { type: 'string', minLength: 1 },
503
+ mime: { type: 'string', minLength: 1, description: 'image/png, image/jpeg, image/svg+xml…' },
504
+ file: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$' },
505
+ },
506
+ required: ['name', 'mime', 'file'],
507
+ additionalProperties: false,
508
+ },
509
+ },
510
+ thumbnail: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$', description: 'A preview image beside theme.json.' },
511
+ },
512
+ required: ['name', 'tokens'],
513
+ additionalProperties: false,
514
+ }
515
+
516
+ const UUID_PATTERN = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
517
+
518
+ /** The experience features a Chat App can offer (`CHAT_APP_FEATURES` in the service's `chat-app-config.ts`). */
519
+ export const CHAT_APP_FEATURES = ['fileUpload', 'webBrowsing', 'voice', 'artifacts', 'canvas', 'followUps', 'workDetails', 'overview', 'fork', 'share', 'export', 'schedule', 'labels', 'feedback', 'humanHandoff'] as const
520
+
521
+ const FEATURE_WORDS: Record<(typeof CHAT_APP_FEATURES)[number], string> = {
522
+ fileUpload: 'people may attach files',
523
+ webBrowsing: 'its Agents may search the web',
524
+ voice: 'voice input',
525
+ artifacts: 'its Agents may make files (documents, decks, sheets)',
526
+ canvas: 'the canvas',
527
+ followUps: 'suggested follow-up questions',
528
+ workDetails: 'people may open the details of the work an Agent did (the steps it takes; a step that starts a procedure shows its name, never its guidance). Off unless set to true: leave it out, or false, for an App customers use',
529
+ overview: 'the conversation overview',
530
+ fork: 'people may fork a Session',
531
+ share: 'people may share a Session',
532
+ export: 'people may export a conversation',
533
+ schedule: 'scheduled tasks',
534
+ labels: 'Session labels',
535
+ feedback: 'thumbs up and down on replies',
536
+ humanHandoff: 'an Agent may hand the conversation to a person (stored as intent; shown where the surface has a control)',
537
+ }
538
+
539
+ /** `chat-apps/<slug>.json`, as `local dev` saves it (`applyChatApp` in `local-chat-apps.ts`; `chatAppConfigSchema` in the service). */
540
+ const CHAT_APP_SCHEMA: JsonSchema = {
541
+ $schema: DRAFT,
542
+ title: 'Chat App (chat-apps/<slug>.json)',
543
+ description:
544
+ 'The experience people use an Agent in: what they see first, the roster of Agents, which features it offers, and its conversation and memory behaviour. `frontera local dev` creates the App by its file name (the slug) and saves the rest as its draft; every section is optional and a key left out keeps its default. A key the platform does not know is dropped, and `local dev` warns about it. Each feature is a request: it is also limited by what the Agent itself allows. Who may open the App and its addresses are set on the platform.',
545
+ type: 'object',
546
+ properties: {
547
+ name: { type: 'string', minLength: 1, description: 'The App’s name; the file name when absent.' },
548
+ description: { type: 'string', description: 'One line about it, on its card.' },
549
+ schemaVersion: { const: 1 },
550
+ presentation: {
551
+ type: 'object',
552
+ description: 'What people see first.',
553
+ properties: {
554
+ welcomeTitle: { type: 'string', maxLength: 120 },
555
+ welcomeMessage: { type: 'string', maxLength: 2000 },
556
+ suggestedPrompts: { type: 'array', maxItems: 8, items: { type: 'string', minLength: 1, maxLength: 300 }, description: 'Starters shown under the welcome.' },
557
+ context: { type: 'string', maxLength: 4000, description: 'The situation the App puts its Agents in ("Customers writing from the portal"): added to each turn, not a system prompt.' },
558
+ accent: { anyOf: [{ enum: ['blue', 'violet', 'green', 'amber', 'rose', 'teal', 'slate'] }, { type: 'null' }] },
559
+ },
560
+ additionalProperties: false,
561
+ },
562
+ roster: {
563
+ type: 'object',
564
+ description: 'The Agents people talk to in it.',
565
+ properties: {
566
+ mode: { enum: ['single', 'multiple'] },
567
+ defaultAgentConfigId: { anyOf: [{ type: 'string', pattern: refPattern(['agent']) }, { type: 'string', pattern: UUID_PATTERN }, { type: 'null' }], description: '`agent/<slug>`: the Agent a new Session starts with; the first on the roster when absent.' },
568
+ agents: {
569
+ type: 'array',
570
+ maxItems: 20,
571
+ items: {
572
+ type: 'object',
573
+ properties: {
574
+ agentConfigId: { anyOf: [{ type: 'string', pattern: refPattern(['agent']) }, { type: 'string', pattern: UUID_PATTERN }], description: '`agent/<slug>`: agents/<slug>/ (or a platform id a pull could not name). `local dev` resolves it to this runtime’s Agent.' },
575
+ label: { anyOf: [{ type: 'string', maxLength: 80 }, { type: 'null' }], description: 'The name people see here; the Agent’s own when absent.' },
576
+ description: { anyOf: [{ type: 'string', maxLength: 300 }, { type: 'null' }], description: 'The line people see here; the Agent’s own when absent.' },
577
+ },
578
+ required: ['agentConfigId'],
579
+ additionalProperties: false,
580
+ },
581
+ },
582
+ allowSwitching: { type: 'boolean', description: 'People may pick another roster Agent for a new Session (mode multiple).' },
583
+ allowMentions: { type: 'boolean', description: '`@Agent` mentions in Agent mode.' },
584
+ agentModeEnabled: { type: 'boolean', description: 'Offer the persistent Agent conversation for roster Agents that allow it.' },
585
+ },
586
+ additionalProperties: false,
587
+ },
588
+ features: {
589
+ type: 'object',
590
+ description: 'Which features the App offers; false withholds one. A feature left out keeps the platform default: on for fileUpload, webBrowsing, artifacts, canvas, followUps, overview, export and feedback; off for voice, workDetails, fork, share, schedule, labels and humanHandoff.',
591
+ properties: Object.fromEntries(CHAT_APP_FEATURES.map((f) => [f, { type: 'boolean', description: FEATURE_WORDS[f] }])),
592
+ additionalProperties: false,
593
+ },
594
+ conversation: {
595
+ type: 'object',
596
+ properties: {
597
+ multipleSessions: { type: 'boolean', description: 'Offer many Sessions, or only the standing Agent conversation.' },
598
+ history: { type: 'boolean', description: 'Show the App’s Session history.' },
599
+ allowClear: { type: 'boolean', description: 'A person may clear their own history.' },
600
+ defaultApprovalMode: { enum: ['ask', 'auto', 'full'], description: 'Never wider than what the person, the Agent or the workspace permits.' },
601
+ },
602
+ additionalProperties: false,
603
+ },
604
+ variables: {
605
+ type: 'object',
606
+ description: "Values the App gives a new conversation's variables, by the Agent's variable name, from the signed-in person. A draft preview uses each variable's testValue instead.",
607
+ propertyNames: { pattern: '^[a-z][a-z0-9_]{0,63}$' },
608
+ maxProperties: 20,
609
+ additionalProperties: {
610
+ type: 'object',
611
+ properties: {
612
+ from: { enum: ['user.id', 'user.email'], description: 'The signed-in person’s id, or their email (used only once it is verified).' },
613
+ lookup: {
614
+ anyOf: [
615
+ {
616
+ type: 'object',
617
+ description: 'Find the one record of objectType whose matchProperty equals the value; the variable takes its property.',
618
+ properties: {
619
+ objectType: { type: 'string', minLength: 1, description: 'The Blueprint object type, e.g. Customer.' },
620
+ matchProperty: { type: 'string', minLength: 1, description: 'The property that holds the person’s id or email, e.g. email.' },
621
+ property: { type: 'string', minLength: 1, description: 'The property the variable takes, e.g. id.' },
622
+ },
623
+ required: ['objectType', 'matchProperty', 'property'],
624
+ additionalProperties: false,
625
+ },
626
+ { type: 'null' },
627
+ ],
628
+ },
629
+ },
630
+ required: ['from'],
631
+ additionalProperties: false,
632
+ },
633
+ },
634
+ memory: {
635
+ type: 'object',
636
+ properties: {
637
+ enabled: { type: 'boolean', description: 'Keep long-term memory for this App (only for an Agent that has memory).' },
638
+ userCanInspect: { type: 'boolean' },
639
+ userCanClear: { type: 'boolean' },
640
+ },
641
+ additionalProperties: false,
642
+ },
643
+ },
644
+ additionalProperties: false,
645
+ }
646
+
647
+ /** `knowledge/<name>/knowledge.json`, as `local dev` applies it (`applyKnowledge` in `local-dev-references.ts`). */
648
+ const KNOWLEDGE_SCHEMA: JsonSchema = {
649
+ $schema: DRAFT,
650
+ title: 'Knowledge base (knowledge/<name>/knowledge.json)',
651
+ description:
652
+ 'A knowledge base: the folder’s documents (Markdown, text, PDF, Word, HTML, in subfolders too) beside this file are uploaded into it by `frontera local dev`, which creates the base by `name` when the runtime has none and sets the settings given here; a setting left out keeps the platform’s default. An Agent searches it once its agent.json lists `{ "knowledgeConfigId": "knowledge/<name>" }` in knowledgeAttachments. Quality checks go in quality-checks.yaml beside it.',
653
+ type: 'object',
654
+ properties: {
655
+ name: { type: 'string', minLength: 1, description: 'How the base is named, and how `knowledge/<name>` finds it; the folder name when absent.' },
656
+ description: { type: 'string', description: 'What it holds, in a sentence: an Agent reads it when deciding to search.' },
657
+ retrieval: { enum: ['contextual', 'always'], description: 'contextual (the default): searched when a turn needs it; always: searched on every turn.' },
658
+ chunker: { enum: ['paragraph', 'sentence', 'markdown', 'manual', 'laws', 'qa', 'spreadsheet'], description: 'How documents are split; paragraph when absent.' },
659
+ chunkSize: { type: 'integer', minimum: 1, description: 'Characters per chunk; 2048 when absent.' },
660
+ chunkOverlap: { type: 'integer', minimum: 0, description: 'Characters shared by neighbouring chunks; 200 when absent.' },
661
+ topK: { type: 'integer', minimum: 1, description: 'How many passages a search returns; 4 when absent.' },
662
+ similarityThreshold: {
663
+ type: 'number',
664
+ minimum: 0,
665
+ maximum: 2,
666
+ description:
667
+ 'The match threshold, as a maximum cosine distance from 0 to 2: a passage is found when its distance is at most this, so lower is stricter. 0.5 when absent. A search\'s score is 1 − distance / 2, so a threshold t keeps scores ≥ 1 − t / 2: 0.5 keeps scores ≥ 0.75, 0.75 keeps ≥ 0.625, 1 keeps ≥ 0.5. `frontera knowledge search <base> "<question>"` shows each passage\'s score and distance and the cutoff the base applies.',
668
+ },
669
+ hybridConfig: {
670
+ anyOf: [{ type: 'null' }, {
671
+ type: 'object',
672
+ description: 'Keyword search beside meaning search, and reranking.',
673
+ properties: { enabled: { type: 'boolean' }, rerank: { type: 'boolean' }, rerankerModel: { type: 'string' }, finalTopK: { type: 'integer', minimum: 1 } },
674
+ }],
675
+ },
676
+ extractorConfig: { anyOf: [{ type: 'null' }, { type: 'object', description: 'Reads structure out of documents with a model.', properties: { enabled: { type: 'boolean' }, model: { type: 'string' } } }] },
677
+ ocrConfig: { anyOf: [{ type: 'null' }, { type: 'object', description: 'Reads scanned PDFs with a vision model.', properties: { enabled: { type: 'boolean' }, model: { type: 'string' }, threshold: { type: 'number' }, batchSize: { type: 'integer', minimum: 1 } } }] },
678
+ },
679
+ additionalProperties: false,
680
+ }
681
+
682
+ /** The front matter of `skills/<name>/SKILL.md`, as `skill push` reads it (`bundle-commands.ts`; the service's skill import). */
683
+ const SKILL_SCHEMA: JsonSchema = {
684
+ $schema: DRAFT,
685
+ title: 'Skill (the front matter of skills/<name>/SKILL.md)',
686
+ description:
687
+ 'A skill: instructions an Agent loads when a task calls for them. SKILL.md opens with YAML front matter between `---` lines (described here), then the instructions in Markdown. Files beside it travel with it: refs/ or references/ (Markdown the skill reads), scripts/ (text scripts, never run by the platform) and assets (images, fonts, sheets). `frontera local dev` pushes the folder; an Agent uses it once its agent.json binds it. A key not listed here is ignored.',
688
+ type: 'object',
689
+ properties: {
690
+ name: { type: 'string', minLength: 1, description: 'The skill’s identity in the workspace, lowercase with hyphens by convention (`dispatch-triage`): what agent.json binds as `skill/<name>`. Keep it the same as the folder name.' },
691
+ description: { type: 'string', minLength: 1, description: 'What it does and when to use it, in a sentence or two: the Agent reads this to decide whether to load the skill.' },
692
+ displayName: { type: 'string', description: 'The name people see; the name when absent.' },
693
+ trigger: { enum: ['manual', 'auto', 'keyword'], description: 'manual (the default): loaded when the Agent chooses it; auto: offered on every turn; keyword: loaded when a message holds one of `keywords`.' },
694
+ keywords: { type: 'array', items: { type: 'string', minLength: 1 }, description: 'With trigger keyword: the words that load it.' },
695
+ enabled: { type: 'boolean', description: 'true when absent.' },
696
+ sortOrder: { type: 'integer', description: 'Its place among the workspace’s skills.' },
697
+ visibleAssets: { type: 'array', items: { type: 'string' }, description: 'Paths of assets (beside SKILL.md) people may see and download, not only the Agent.' },
698
+ },
699
+ required: ['name', 'description'],
700
+ }
701
+
702
+ const SECRETS_SCHEMA: JsonSchema = {
703
+ $schema: DRAFT,
704
+ title: 'The secrets a project needs (secrets.json)',
705
+ description:
706
+ 'Names and descriptions only, never values: a value is set on each computer with `frontera secret set NAME` and stays on that runtime. `frontera local dev` and `frontera local check` say each listed secret the local runtime has no value for, and each secret a Function or workflow names that the file leaves out. A `value` is refused and ignored.',
707
+ type: 'object',
708
+ properties: {
709
+ secrets: {
710
+ type: 'array',
711
+ items: {
712
+ type: 'object',
713
+ properties: {
714
+ name: { type: 'string', pattern: '^[A-Z][A-Z0-9_]*$', description: 'The secret’s name, as a Function’s `secret:NAME` grant or a workflow’s `auth.secret` names it.' },
715
+ description: { type: 'string', description: 'What it is for and where its value comes from, for the person setting it.' },
716
+ },
717
+ required: ['name'],
718
+ additionalProperties: false,
719
+ },
720
+ },
721
+ },
722
+ required: ['secrets'],
723
+ additionalProperties: false,
724
+ }
725
+
211
726
  /** The schema this CLI owns for a kind, or null when the service owns it. */
212
727
  export function cliSchema(kind: SchemaKind): JsonSchema | null {
213
728
  if (kind === 'evals') return EVALS_SCHEMA
729
+ if (kind === 'workflow-tests') return WORKFLOW_TESTS_SCHEMA
214
730
  if (kind === 'dataset') return DATASET_SCHEMA
731
+ if (kind === 'bindings') return BINDINGS_SCHEMA
732
+ if (kind === 'theme') return THEME_SCHEMA
733
+ if (kind === 'chat-app') return CHAT_APP_SCHEMA
734
+ if (kind === 'knowledge') return KNOWLEDGE_SCHEMA
735
+ if (kind === 'skill') return SKILL_SCHEMA
736
+ if (kind === 'secrets') return SECRETS_SCHEMA
737
+ if (kind in BLUEPRINT_SCHEMAS) return BLUEPRINT_SCHEMAS[kind as keyof typeof BLUEPRINT_SCHEMAS]
215
738
  return null
216
739
  }
217
740
 
741
+ // ── The service's formats, as a project file may write them ─────────────────
742
+
743
+ /**
744
+ * Where a project file names another resource, the runtime's schema wants
745
+ * its id (a uuid), and `local dev` accepts the project's reference instead
746
+ * (`kind/name`, read by `parseRef`, resolved by `local-dev-references.ts`
747
+ * before the file is applied). These say which kinds each place may name.
748
+ */
749
+
750
+ /** A workflow step's (or trigger's) `resourceId`, by its `kind`. */
751
+ const STEP_REFS: Record<string, { kinds: RefKind[]; words: string }> = {
752
+ blueprint: { kinds: ['objectType'], words: 'the object type it reads: `objectType/<ApiName>`' },
753
+ function: { kinds: ['function'], words: 'the Function it runs: `function/<slug>`' },
754
+ llm: { kinds: ['model'], words: 'the model it asks: `model/<slug>`, a slug from the runtime’s model catalog (`frontera local models-available`); a model the runtime lacks is swapped for its default, and `local dev` says so' },
755
+ action: { kinds: ['action'], words: 'the governed Action it runs: `action/<apiName>`' },
756
+ agent: { kinds: ['agent'], words: 'the Agent it asks: `agent/<slug>`' },
757
+ plugin: { kinds: ['plugin'], words: 'the Plugin it calls: `plugin/<name>`' },
758
+ }
759
+
760
+ /** A uuid field elsewhere, by its key. */
761
+ const KEY_REFS: Record<string, RefKind[]> = {
762
+ objectTypeId: ['objectType'],
763
+ objectSetId: ['objectSet'],
764
+ functionId: ['function'],
765
+ workflowId: ['workflow'],
766
+ agentId: ['agent'],
767
+ actionId: ['action'],
768
+ knowledgeBaseId: ['knowledge'],
769
+ skillId: ['skill'],
770
+ }
771
+
772
+ const isRecord = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v)
773
+
774
+ /** A uuid field that also takes the project's reference to one of these kinds. */
775
+ function refOrId(node: Record<string, unknown>, kinds: readonly RefKind[], words?: string): Record<string, unknown> {
776
+ const { description, ...id } = node
777
+ const said = words ?? `${kinds.length === REF_KINDS.length ? 'a resource' : kinds.map((k) => `\`${k}/<name>\``).join(' or ')}`
778
+ return {
779
+ description: `${typeof description === 'string' ? `${description} ` : ''}In a project file: ${said}, which \`frontera local dev\` resolves to this runtime's id; or the id itself.`,
780
+ anyOf: [{ type: 'string', pattern: refPattern(kinds) }, id],
781
+ 'x-frontera-reference': [...kinds],
782
+ }
783
+ }
784
+
785
+ /** A property's schema with a description added (or replaced), and optionally more of its shape. */
786
+ function describe(props: Record<string, unknown>, key: string, description: string, extra: Record<string, unknown> = {}): void {
787
+ const node = props[key]
788
+ if (!isRecord(node)) return
789
+ props[key] = { ...node, ...extra, description }
790
+ }
791
+
792
+ /**
793
+ * agent.json `config.features.computerAvailability`, as the service reads it
794
+ * (`AGENT_COMPUTER_AVAILABILITY` and `resolveComputerRuntimePolicy` in
795
+ * `computer-runtime-policy.ts`): the service's schema keeps `features` an open
796
+ * map, so the key the CLI's own fix names is said here.
797
+ */
798
+ const COMPUTER_AVAILABILITY = {
799
+ type: 'string',
800
+ enum: ['none', 'on-demand', 'eager'],
801
+ description:
802
+ 'The Agent’s Computer availability (the Agent’s Advanced page, Computer): `none` gives it no computer (no shell, files or artifacts; an attachment goes to the model directly); `on-demand` starts one the first time a turn needs it (a tool, an attachment to put on it, its memory); `eager` keeps one ready before every turn. When absent, an Agent with behavior set up (agents/<slug>/operating.json) has none and any other Agent has `eager`; a value not listed counts as `none`. An Agent with behavior is offered its Plugins (agent.json `plugins`) only with `on-demand` or `eager`. A runtime with no computers to start (most local runtimes) runs the Agent without one whatever this says, and `frontera local status` says so; the setting still applies where the Agent is published.',
803
+ }
804
+
805
+ /**
806
+ * What the service's schemas leave unsaid about a project file, said where an
807
+ * editor and a coding agent read it: the forms a procedure's tool list takes,
808
+ * how an Agent names its knowledge, and which agent.json lists do not hold
809
+ * what an Agent may start or read (that is bindings.json). Recognized by the
810
+ * properties around them, so it holds for any release that has them.
811
+ * Idempotent.
812
+ */
813
+ function describeProjectFields(props: Record<string, unknown>): void {
814
+ // A procedure (operating.json `procedures[]`): its own tools and knowledge.
815
+ if ('tools' in props && 'knowledge' in props && 'actions' in props) {
816
+ const tools = props.tools
817
+ if (isRecord(tools) && isRecord(tools.properties)) {
818
+ const inner = { ...tools.properties }
819
+ describe(inner, 'ids', `With \`inherit: false\`, the only tools the procedure keeps, each one of: ${PROCEDURE_TOOL_FORMS}. \`frontera local dev\` resolves a slug to this runtime's id, refuses another form, and warns about a Function or workflow the Agent is not bound to.`, {
820
+ items: { type: 'string', minLength: 1, maxLength: 64, pattern: `^(function:[A-Za-z0-9][A-Za-z0-9._-]*|workflow:[A-Za-z0-9][A-Za-z0-9._-]*|tool:(${PROCEDURE_TOOL_GROUPS.join('|')}))$` },
821
+ })
822
+ props.tools = { ...tools, properties: inner, description: 'The tools while the procedure is active: the Agent’s own (`inherit: true`, the default), or only those in `ids`.' }
823
+ }
824
+ const knowledge = props.knowledge
825
+ if (isRecord(knowledge) && isRecord(knowledge.properties)) {
826
+ const inner = { ...knowledge.properties }
827
+ describe(inner, 'ids', 'With `inherit: false`, the only knowledge bases the procedure searches: `knowledge/<name>` each, attached to the Agent in agent.json `knowledgeAttachments`.')
828
+ props.knowledge = { ...knowledge, properties: inner }
829
+ }
830
+ }
831
+ // How an Agent runs (operating.json `execution`): whether it has the governed Action tools.
832
+ if ('harness' in props && 'toolScopes' in props) {
833
+ describe(props, 'toolScopes', 'Optional; 2 when present. With it, `tool:blueprint` is the Blueprint query tools only, the governed Action tools need `tool:blueprint-actions`, and `blueprintActions` decides whether the Agent has them outside a procedure that lists its own tools. Without it the file is read as before Actions were their own choice: `tool:blueprint` keeps the Action tools, and a service Agent has them outside procedures. New Agents are scaffolded with it; `frontera local dev` warns about a file without it.', { const: 2 })
834
+ }
835
+ if ('harness' in props && 'blueprintActions' in props) {
836
+ describe(props, 'blueprintActions', 'With `toolScopes: 2`: whether the Agent has the governed Action tools (list Actions, submit one, read a request) outside a procedure that lists its own tools: on when absent for an open-ended Agent, off for a service (customer-facing) one. A procedure that keeps its own tools offers them only with `tool:blueprint-actions` in `tools.ids`. `tool:blueprint` is the query tools alone. Either needs an object type granted in bindings.json.')
837
+ }
838
+ // Behavior settings (operating.json): which records a conversation reads.
839
+ if ('dataScopes' in props && 'variables' in props) {
840
+ describe(props, 'dataScopes', 'Optional. Narrows what the Agent reads of an object type to the records of the conversation: `[{ "objectType": "WorkOrder", "property": "customerId", "equals": "{{customer_id}}" }]`, at most one per object type. Every read of that type (query, fetch, aggregate, saved sets, links, media and metrics) sees only rows whose property equals the conversation variable; a conversation without the variable sees none. The variable is one in `variables`, and the property and variable are text or numbers. The object type must be granted in bindings.json `blueprint`. In an eval case, `variables` gives the conversation its values.')
841
+ }
842
+ // An Agent (agent.json): the lists that name other resources.
843
+ if ('modelConfigs' in props && 'knowledgeAttachments' in props) {
844
+ describe(props, 'knowledgeAttachments', 'The knowledge bases the Agent searches: `[{ "knowledgeConfigId": "knowledge/<name>" }]`, one per base in knowledge/<name>/knowledge.json (`retrievalOverride` optional). `local dev` resolves each to this runtime’s id. When the key is present it is the Agent’s whole list (a base it leaves out is detached); leave the key out to keep the attachments made with `frontera knowledge attach`.', {
845
+ items: {
846
+ type: 'object',
847
+ properties: {
848
+ knowledgeConfigId: { type: 'string', description: '`knowledge/<name>`: the base’s name as knowledge/<name>/knowledge.json gives it (words allowed), or its id.' },
849
+ retrievalOverride: { description: 'How this Agent retrieves from the base, when not the base’s own setting.' },
850
+ },
851
+ required: ['knowledgeConfigId'],
852
+ },
853
+ })
854
+ describe(props, 'objectBindings', 'Object definitions (the workspace’s objects, not Blueprint object types) the Agent reads and writes through its object tools: each row `objectDefinitionId: object/<slug>` with its `definition` (name, fields). The Blueprint object types it may query are granted in agents/<slug>/bindings.json `blueprint`, not here.')
855
+ describe(props, 'functions', 'Not applied: these rows are no longer run. The Functions and workflows the Agent may start are its bindings, in agents/<slug>/bindings.json (`frontera local schema bindings`).')
856
+ // `config.features`: the Agent's own Computer availability, kept beside the on/off switches (the service's `computer-runtime-policy.ts`).
857
+ const config = props.config
858
+ const features = isRecord(config) && isRecord(config.properties) ? config.properties.features : undefined
859
+ if (isRecord(config) && isRecord(config.properties) && isRecord(features) && !(isRecord(features.properties) && 'computerAvailability' in features.properties)) {
860
+ props.config = {
861
+ ...config,
862
+ properties: {
863
+ ...config.properties,
864
+ features: {
865
+ ...features,
866
+ description: 'On/off switches (`true` or `false` each, such as `autoTitle`), and the Agent’s Computer availability in `computerAvailability`.',
867
+ properties: { ...(isRecord(features.properties) ? features.properties : {}), computerAvailability: COMPUTER_AVAILABILITY },
868
+ },
869
+ },
870
+ }
871
+ }
872
+ // A project file's form of `appCapabilities`: capability ids belong to one install, so a pull names each by Plugin and tool.
873
+ if ('appCapabilities' in props && !('pluginTools' in props)) {
874
+ props.pluginTools = {
875
+ type: 'array',
876
+ description: 'The Agent’s own settings for its Plugins’ tools, by Plugin and tool name: each row gives the Agent a tool its Plugin marks sensitive, or sets whether a call to it needs approval. A pull writes it from the platform’s `appCapabilities`, whose capability ids belong to one install. `frontera local dev` turns each row into this runtime’s capability; a row whose Plugin the runtime doesn’t have is left off, and a tool the Plugin doesn’t list here is left off with a warning. When present it replaces `appCapabilities`. The Plugin itself is attached in `appInstalls`.',
877
+ items: {
878
+ type: 'object',
879
+ properties: {
880
+ plugin: { type: 'string', pattern: '^plugin/.+', description: '`plugin/<name>`: the Plugin, by the name its file in mcp/ gives it (mcp/<name>.plugin.json, or a stand-in, mcp/<name>.stand-in.json).' },
881
+ tool: { type: 'string', description: 'The tool’s name, as the Plugin lists it.' },
882
+ accountMode: { type: 'string', enum: ['agent_owned', 'end_user'], description: 'Whose connection the tool uses: the Agent’s own (`agent_owned`, the default) or the person in the conversation’s (`end_user`).' },
883
+ requiresApprovalOverride: { type: ['boolean', 'null'], description: 'Whether a call needs approval, in place of the Plugin’s own setting; leave it out to keep that setting.' },
884
+ },
885
+ required: ['plugin', 'tool'],
886
+ },
887
+ }
888
+ }
889
+ }
890
+ }
891
+
892
+ /**
893
+ * A service schema as a project file may be written: every id field also
894
+ * takes the reference `local dev` resolves (per step kind for a workflow's
895
+ * `resourceId`, per key elsewhere, any kind otherwise), and a skill binding's
896
+ * `workspaceId` is optional, since `local dev` fills in the runtime's own.
897
+ * Idempotent: a field it already widened is left as it is.
898
+ */
899
+ export function localizeServiceSchema(schema: JsonSchema): JsonSchema {
900
+ const visit = (node: unknown, key: string, stepKind: string | null): unknown => {
901
+ if (Array.isArray(node)) return node.map((n) => visit(n, key, stepKind))
902
+ if (!isRecord(node)) return node
903
+ if (node['x-frontera-reference']) return node
904
+ if (node.format === 'uuid' && (node.type === 'string' || node.type === undefined)) {
905
+ const step = key === 'resourceId' && stepKind ? STEP_REFS[stepKind] : undefined
906
+ if (step) return refOrId(node, step.kinds, step.words)
907
+ return refOrId(node, KEY_REFS[key] ?? REF_KINDS)
908
+ }
909
+ const props = isRecord(node.properties) ? node.properties : null
910
+ const kindConst = props && isRecord(props.kind) && typeof props.kind.const === 'string' ? props.kind.const : null
911
+ const out: Record<string, unknown> = {}
912
+ for (const [k, v] of Object.entries(node)) {
913
+ if (k === 'properties' && props) {
914
+ out.properties = Object.fromEntries(Object.entries(props).map(([name, child]) => [name, visit(child, name, kindConst ?? stepKind)]))
915
+ continue
916
+ }
917
+ // Keywords that hold schemas of the same value (anyOf, oneOf, items…) keep the key they describe.
918
+ out[k] = visit(v, k === '$defs' || k === 'definitions' ? '' : ['anyOf', 'oneOf', 'allOf', 'items', 'additionalProperties', 'not', 'prefixItems'].includes(k) ? key : k, k === '$defs' ? null : stepKind)
919
+ }
920
+ // A skill binding: the workspace it lives in is this runtime's own, filled in by `local dev`.
921
+ if (props && 'workspaceId' in props && 'workspaceSkillId' in props && Array.isArray(out.required)) {
922
+ out.required = (out.required as unknown[]).filter((r) => r !== 'workspaceId')
923
+ const ws = (out.properties as Record<string, unknown>).workspaceId
924
+ if (isRecord(ws)) (out.properties as Record<string, unknown>).workspaceId = { ...ws, description: 'Leave it out in a project file: `frontera local dev` fills in the runtime’s workspace.' }
925
+ const skill = (out.properties as Record<string, unknown>).workspaceSkillId
926
+ if (isRecord(skill) && !skill.description) (out.properties as Record<string, unknown>).workspaceSkillId = { ...skill, description: 'In a project file: `skill/<slug>`, or the id.' }
927
+ }
928
+ if (props) describeProjectFields(out.properties as Record<string, unknown>)
929
+ return out
930
+ }
931
+ return visit(schema, '', null) as JsonSchema
932
+ }
933
+
218
934
  export function schemasDir(project: LocalProject): string {
219
935
  return join(project.stateDir, 'schemas')
220
936
  }
@@ -268,7 +984,8 @@ export function readKeptSchema(project: LocalProject, kind: SchemaKind): JsonSch
268
984
  if (!existsSync(path)) return null
269
985
  try {
270
986
  const value = JSON.parse(readFileSync(path, 'utf8')) as unknown
271
- return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonSchema) : null
987
+ // One kept by an earlier CLI, before references were described, is described as it is read.
988
+ return value && typeof value === 'object' && !Array.isArray(value) ? (cliSchema(kind) ? (value as JsonSchema) : localizeServiceSchema(value as JsonSchema)) : null
272
989
  } catch {
273
990
  return null
274
991
  }
@@ -287,7 +1004,7 @@ export async function keepSchemas(project: LocalProject, source: SchemaSource):
287
1004
  let schema = cliSchema(kind)
288
1005
  if (!schema) {
289
1006
  const fetched = await fetchServiceSchema(source, kind)
290
- if (fetched.ok) schema = fetched.schema
1007
+ if (fetched.ok) schema = localizeServiceSchema(fetched.schema)
291
1008
  else problems.push(`${kind}: ${fetched.reason}`)
292
1009
  }
293
1010
  if (schema) writeFileAtomic(schemaPath(project, kind), `${JSON.stringify(schema, null, 2)}\n`, 0o644)
@@ -320,7 +1037,7 @@ export async function resolveSchema(project: LocalProject, kind: SchemaKind, sou
320
1037
  }
321
1038
  }
322
1039
  const fetched = await fetchServiceSchema(source, kind)
323
- if (fetched.ok) return { ok: true, schema: fetched.schema, from: 'runtime' }
1040
+ if (fetched.ok) return { ok: true, schema: localizeServiceSchema(fetched.schema), from: 'runtime' }
324
1041
  if (kept) return { ok: true, schema: kept, from: 'kept', note: `The local runtime did not answer (${fetched.reason}): this is the format its last start kept.` }
325
1042
  return {
326
1043
  ok: false,