@frontera-sdk/cli 1.50.79 → 1.50.81
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/README.md +6 -0
- package/package.json +4 -4
- package/src/api/blueprint-authoring-api.ts +52 -0
- package/src/api/credential-failure.ts +34 -4
- package/src/api/dataset-api.ts +1 -1
- package/src/api/governed-action-api.ts +75 -7
- package/src/api/media-api.ts +181 -0
- package/src/api/platform-api.ts +59 -0
- package/src/api/workflow-api.ts +1 -1
- package/src/auth-verify.ts +1 -1
- package/src/commands/action/available.ts +59 -0
- package/src/commands/action/cancel.ts +34 -0
- package/src/commands/action/decide.ts +54 -0
- package/src/commands/action/deploy.ts +4 -1
- package/src/commands/action/index-commands.ts +16 -6
- package/src/commands/action/prepare.ts +3 -1
- package/src/commands/action/request-summary.ts +13 -0
- package/src/commands/action/requests.ts +6 -5
- package/src/commands/action/submit.ts +183 -0
- package/src/commands/blueprint/get.ts +139 -29
- package/src/commands/chat-app/index-commands.ts +475 -0
- package/src/commands/knowledge/upload-plan.ts +14 -8
- package/src/commands/marking/index-commands.ts +171 -0
- package/src/commands/media/index-commands.ts +592 -0
- package/src/commands/registry.ts +28 -6
- package/src/commands/types.ts +6 -0
- package/src/commands/workflow/list.ts +1 -1
- package/src/errors.ts +3 -2
- package/src/exit.ts +126 -1
- package/src/flag-help.ts +24 -2
- package/src/forge/touches.ts +2 -0
- package/src/harness.ts +5 -3
- package/src/main.ts +23 -38
- package/src/scopes.ts +43 -0
- package/src/template.ts +6 -4
- package/src/templates/next-app-files.ts +8 -4
- package/src/vendor/sdk-sources.json +16 -15
package/src/commands/registry.ts
CHANGED
|
@@ -31,8 +31,11 @@ import { capabilityCommands } from './capability/index-commands'
|
|
|
31
31
|
import { datasetCommands } from './dataset/index-commands'
|
|
32
32
|
import { sourceCommands } from './source/index-commands'
|
|
33
33
|
import { knowledgeCommands } from './knowledge/index-commands'
|
|
34
|
+
import { mediaCommands } from './media/index-commands'
|
|
34
35
|
import { packCommands } from './pack/index-commands'
|
|
35
36
|
import { secretCommands } from './secret/index-commands'
|
|
37
|
+
import { chatAppCommands } from './chat-app/index-commands'
|
|
38
|
+
import { markingCommands } from './marking/index-commands'
|
|
36
39
|
import { automationCommands } from './automation/index-commands'
|
|
37
40
|
import { workflowCommands } from './workflow/index-commands'
|
|
38
41
|
import { completionCommand } from './completion'
|
|
@@ -102,8 +105,10 @@ export const COMMANDS: readonly Command[] = [
|
|
|
102
105
|
...datasetCommands,
|
|
103
106
|
...sourceCommands,
|
|
104
107
|
...knowledgeCommands,
|
|
108
|
+
...mediaCommands,
|
|
105
109
|
...packCommands,
|
|
106
110
|
...secretCommands,
|
|
111
|
+
...chatAppCommands,
|
|
107
112
|
...automationCommands,
|
|
108
113
|
...workflowCommands,
|
|
109
114
|
|
|
@@ -121,6 +126,7 @@ export const COMMANDS: readonly Command[] = [
|
|
|
121
126
|
blueprintBind,
|
|
122
127
|
blueprintEditable,
|
|
123
128
|
blueprintGrant,
|
|
129
|
+
...markingCommands,
|
|
124
130
|
|
|
125
131
|
...actionCommands,
|
|
126
132
|
]
|
|
@@ -164,9 +170,11 @@ const NOUN_SUMMARY: Readonly<Record<string, string>> = {
|
|
|
164
170
|
knowledge: 'Knowledge bases and the sources inside them',
|
|
165
171
|
pack: 'Reusable skill bundles — author once, install per workspace',
|
|
166
172
|
secret: 'Workspace secrets — named here, never printed back',
|
|
173
|
+
'chat-app': 'Chat Apps — stage, publish and open an Agent roster',
|
|
167
174
|
function: 'Functions — TypeScript deployed here, run on a schedule',
|
|
168
175
|
workflow: 'Workflows — declared steps, versioned, promoted, then run',
|
|
169
176
|
blueprint: 'The shared model of the organization — what an app can read',
|
|
177
|
+
marking: 'Security markings — who may see which marked Blueprint rows',
|
|
170
178
|
action: 'Governed Actions — arm the write path a published Action runs',
|
|
171
179
|
capability: 'What an agent may DO — plugin operations granted to it',
|
|
172
180
|
auth: 'Credential profiles — one per API key, selected by directory',
|
|
@@ -179,6 +187,11 @@ export function nounSummary(noun: string): string {
|
|
|
179
187
|
return NOUN_SUMMARY[noun] ?? ''
|
|
180
188
|
}
|
|
181
189
|
|
|
190
|
+
const KEYS_ARE_SETTINGS_ONLY =
|
|
191
|
+
'organization keys are managed in Organization settings → API Keys, workspace keys in '
|
|
192
|
+
+ 'Workspace settings → API Keys — by a signed-in administrator, never by the CLI: a '
|
|
193
|
+
+ 'credential that mints credentials is the escalation that separation exists to prevent'
|
|
194
|
+
|
|
182
195
|
/**
|
|
183
196
|
* Where the CLI ends, and what to do instead.
|
|
184
197
|
*
|
|
@@ -202,8 +215,12 @@ export const UNSUPPORTED: Readonly<Record<string, string>> = {
|
|
|
202
215
|
channel:
|
|
203
216
|
'channels have no CLI surface — create one and bind it to an agent in the Console, '
|
|
204
217
|
+ 'under the agent’s Channels tab',
|
|
205
|
-
key:
|
|
206
|
-
|
|
218
|
+
key: KEYS_ARE_SETTINGS_ONLY,
|
|
219
|
+
// The names a caller reaches for when the goal is provisioning or rotating an
|
|
220
|
+
// organization key. The service routes are session-only by construction, so
|
|
221
|
+
// there is nothing for a key-holding CLI to call.
|
|
222
|
+
'org-key': KEYS_ARE_SETTINGS_ONLY,
|
|
223
|
+
'api-key': KEYS_ARE_SETTINGS_ONLY,
|
|
207
224
|
sheet: 'sheets have no CLI surface and none is planned',
|
|
208
225
|
task: 'task and run history is Console-only; `frontera function runs` covers function runs',
|
|
209
226
|
eval: 'the evaluation suite (datasets, scorers, judges, queues) is Console-only',
|
|
@@ -262,7 +279,7 @@ const GLOBAL_SECTION = [
|
|
|
262
279
|
['FRONTERA_API_URL', 'Frontera API origin'],
|
|
263
280
|
// Upstream's key-kind detail, kept — profiles change where a key is stored,
|
|
264
281
|
// not which kinds are valid.
|
|
265
|
-
['FRONTERA_TOKEN', 'sk-ws-… or sk-org-… — requires FRONTERA_API_URL or --api-url'],
|
|
282
|
+
['FRONTERA_TOKEN', 'sk-ws-… or sk-org-… key, or a session token for session-only commands — requires FRONTERA_API_URL or --api-url'],
|
|
266
283
|
['FRONTERA_PROFILE', 'profile to use, overriding the directory selection'],
|
|
267
284
|
['FRONTERA_SECRET_STORE', 'where keys are stored: keychain (default on macOS) or file'],
|
|
268
285
|
['NO_COLOR', 'set to disable colour'],
|
|
@@ -370,6 +387,12 @@ export function renderNounHelp(noun: string): string {
|
|
|
370
387
|
return lines.join('\n')
|
|
371
388
|
}
|
|
372
389
|
|
|
390
|
+
/** The help line every session-lane command prints — see `SESSION_LANE_HINT`. */
|
|
391
|
+
export const SESSION_LANE_HELP =
|
|
392
|
+
'Needs a person’s session, not an API key. Set FRONTERA_TOKEN to your session token '
|
|
393
|
+
+ 'and FRONTERA_API_URL (or pass --api-url). sk-ws- and sk-org- keys are refused with '
|
|
394
|
+
+ '401 or 403; `frontera login` stores keys only.'
|
|
395
|
+
|
|
373
396
|
export function renderCommandHelp(meta: CommandMeta): string {
|
|
374
397
|
const width = terminalWidth()
|
|
375
398
|
const lines = [accent(usage(meta)), '', ...wrap(meta.summary, width)]
|
|
@@ -385,8 +408,7 @@ export function renderCommandHelp(meta: CommandMeta): string {
|
|
|
385
408
|
lines.push(
|
|
386
409
|
'',
|
|
387
410
|
...wrap(
|
|
388
|
-
|
|
389
|
-
+ 'refuse sk-ws- and sk-org- keys, and answer 401 rather than a permission error.',
|
|
411
|
+
SESSION_LANE_HELP,
|
|
390
412
|
width,
|
|
391
413
|
).map(dim),
|
|
392
414
|
)
|
|
@@ -456,7 +478,7 @@ export interface CommandDescriptor {
|
|
|
456
478
|
requiresCredential: boolean
|
|
457
479
|
/**
|
|
458
480
|
* `api-key` — reachable with `sk-ws-`/`sk-org-` or a session.
|
|
459
|
-
* `session` — reachable ONLY with a session token
|
|
481
|
+
* `session` — reachable ONLY with a person's session token in `FRONTERA_TOKEN`.
|
|
460
482
|
*/
|
|
461
483
|
authLane: 'api-key' | 'session'
|
|
462
484
|
}
|
package/src/commands/types.ts
CHANGED
|
@@ -68,6 +68,12 @@ export interface CommandMeta {
|
|
|
68
68
|
* that forgets to declare this is far more likely to be on it.
|
|
69
69
|
*/
|
|
70
70
|
authLane?: 'api-key' | 'session'
|
|
71
|
+
/**
|
|
72
|
+
* Session-lane only: where a person does this in the product, e.g.
|
|
73
|
+
* `Blueprint → Access → Markings`. Appended to `SESSION_LANE_HINT` when a key
|
|
74
|
+
* is refused, so a noun names its way out without rewording the refusal.
|
|
75
|
+
*/
|
|
76
|
+
sessionAlternative?: string
|
|
71
77
|
/**
|
|
72
78
|
* Present → the command is planned but unbuilt, and fails with this message.
|
|
73
79
|
* Reserved in the table rather than omitted: a caller reading "unknown
|
|
@@ -16,7 +16,7 @@ export const workflowList: Command = {
|
|
|
16
16
|
verb: 'list',
|
|
17
17
|
args: [],
|
|
18
18
|
flags: {},
|
|
19
|
-
summary: 'List workflows, their live version and what can start them',
|
|
19
|
+
summary: 'List workflows in service, their live version and what can start them. Archived workflows are left out',
|
|
20
20
|
examples: ['frontera workflow list'],
|
|
21
21
|
},
|
|
22
22
|
|
package/src/errors.ts
CHANGED
|
@@ -13,8 +13,9 @@ export class CliError extends Error {
|
|
|
13
13
|
readonly code: string
|
|
14
14
|
readonly hint?: string
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** `cause` keeps the original error — and its `details` — when one is reworded. */
|
|
17
|
+
constructor(message: string, opts: { code: string; hint?: string; cause?: unknown }) {
|
|
18
|
+
super(message, opts.cause === undefined ? undefined : { cause: opts.cause })
|
|
18
19
|
this.name = 'CliError'
|
|
19
20
|
this.code = opts.code
|
|
20
21
|
if (opts.hint) this.hint = opts.hint
|
package/src/exit.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PERSON_FORBIDDEN_HINT } from './api/credential-failure'
|
|
2
|
+
import { CliError, UsageError } from './errors'
|
|
3
|
+
import { commandLabel, commandScope } from './scopes'
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Exit codes, one per action the caller can take.
|
|
@@ -61,6 +63,11 @@ const AUTH_CODES = new Set([
|
|
|
61
63
|
'FORBIDDEN',
|
|
62
64
|
// The key is gone, not merely unselected — same recovery as a revoked one.
|
|
63
65
|
'PROFILE_SECRET_MISSING',
|
|
66
|
+
// Policy refusals the service keeps its own code for: separation of duties,
|
|
67
|
+
// and rows behind a marking the caller is not cleared for. Neither ever
|
|
68
|
+
// succeeds on retry, which is what exit 1 would tell a script to do.
|
|
69
|
+
'ACTION_FORBIDDEN',
|
|
70
|
+
'DATASET_ROWS_COMPARTMENTED',
|
|
64
71
|
])
|
|
65
72
|
|
|
66
73
|
/**
|
|
@@ -212,3 +219,121 @@ export function toEnvelope(err: unknown): ErrorEnvelope {
|
|
|
212
219
|
message: err instanceof Error ? err.message : String(err),
|
|
213
220
|
}
|
|
214
221
|
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The one recovery for a key sent to a command that acts as a person.
|
|
225
|
+
*
|
|
226
|
+
* `frontera login` and `auth add` store only `sk-ws-`/`sk-org-` keys, so the
|
|
227
|
+
* session has to come from the environment. Worded once here because every
|
|
228
|
+
* session-lane noun meets the same refusal — 401 on session-only routers, 403
|
|
229
|
+
* on programmatic ones — and per-API copies of this sentence had started to
|
|
230
|
+
* drift apart.
|
|
231
|
+
*/
|
|
232
|
+
export const SESSION_LANE_HINT =
|
|
233
|
+
'this command acts as a person and refuses API keys — set FRONTERA_TOKEN to your session token '
|
|
234
|
+
+ 'and FRONTERA_API_URL, then re-run; `frontera login` stores keys only'
|
|
235
|
+
|
|
236
|
+
/** What a session-lane command needs to know about itself to word a refusal. */
|
|
237
|
+
export interface SessionLaneMeta {
|
|
238
|
+
noun: string
|
|
239
|
+
verb: string
|
|
240
|
+
scope?: 'workspace' | 'organization'
|
|
241
|
+
authLane?: 'api-key' | 'session'
|
|
242
|
+
sessionAlternative?: string
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The first line a key reads on a session-lane command. */
|
|
246
|
+
export function sessionLaneKeyMessage(meta: { noun: string; verb: string }): string {
|
|
247
|
+
return `\`${commandLabel(meta)}\` acts as a person; API keys are refused`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The hint that replaces a failure's own on a session-lane command, or
|
|
252
|
+
* `undefined` to keep it.
|
|
253
|
+
*
|
|
254
|
+
* A key (`sk-…`) refused with 401/403 gets `SESSION_LANE_HINT`, plus where a
|
|
255
|
+
* person does the same thing in the product when the command names one. A
|
|
256
|
+
* person's session refused with 403 is told the person lacks the permission —
|
|
257
|
+
* the service's generic FORBIDDEN hint says "use a different key", which is
|
|
258
|
+
* the wrong fix for someone who holds no key. A session refused with 401 keeps
|
|
259
|
+
* its own hint: it has expired, and re-signing in is the fix. No token at all
|
|
260
|
+
* means the failure came before a credential resolved, so it is about neither.
|
|
261
|
+
*/
|
|
262
|
+
export function sessionLaneHint(
|
|
263
|
+
meta: Pick<SessionLaneMeta, 'authLane' | 'sessionAlternative'>,
|
|
264
|
+
token: string,
|
|
265
|
+
code: string,
|
|
266
|
+
): string | undefined {
|
|
267
|
+
if (meta.authLane !== 'session' || !token) return undefined
|
|
268
|
+
if (!token.startsWith('sk-')) return code === 'FORBIDDEN' ? PERSON_FORBIDDEN_HINT : undefined
|
|
269
|
+
if (code !== 'UNAUTHORIZED' && code !== 'FORBIDDEN') return undefined
|
|
270
|
+
return meta.sessionAlternative
|
|
271
|
+
? `${SESSION_LANE_HINT}; or do it in ${meta.sessionAlternative}`
|
|
272
|
+
: SESSION_LANE_HINT
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Refused before the request, or `null` to let it go.
|
|
277
|
+
*
|
|
278
|
+
* Any key on a session-lane command, whatever its scope: every such route
|
|
279
|
+
* refuses every key, so nothing is lost by not asking. It also covers a route
|
|
280
|
+
* that answers a key with an empty 200 instead of a refusal, which would read
|
|
281
|
+
* as "you can do nothing here" and exit 0. This trusts `authLane`, so a
|
|
282
|
+
* command is tagged `session` only when EVERY route it calls refuses every
|
|
283
|
+
* key — by mounting session-only auth, or by an authority check no key
|
|
284
|
+
* principal can pass (the governed Action invoke routes join organization
|
|
285
|
+
* and workspace membership). `action deploy` and `action prepare` reach
|
|
286
|
+
* routes a key legitimately uses, and are `api-key`. Then a person's session on a
|
|
287
|
+
* workspace-scoped command that named no workspace, which the service answers
|
|
288
|
+
* with a bare 400. A missing argument outranks the workspace, as it does for
|
|
289
|
+
* the organization-key refusal.
|
|
290
|
+
*/
|
|
291
|
+
export function sessionLaneRefusal(opts: {
|
|
292
|
+
meta: SessionLaneMeta
|
|
293
|
+
token: string
|
|
294
|
+
workspace: string | undefined
|
|
295
|
+
missingArg: boolean
|
|
296
|
+
}): CliError | null {
|
|
297
|
+
const { meta, token, workspace, missingArg } = opts
|
|
298
|
+
if (meta.authLane !== 'session') return null
|
|
299
|
+
if (token.startsWith('sk-')) {
|
|
300
|
+
return new CliError(sessionLaneKeyMessage(meta), {
|
|
301
|
+
code: 'FORBIDDEN',
|
|
302
|
+
hint: sessionLaneHint(meta, token, 'FORBIDDEN')!,
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
if (missingArg || workspace || commandScope(meta) !== 'workspace') return null
|
|
306
|
+
return new UsageError(
|
|
307
|
+
`\`${commandLabel(meta)}\` acts inside a workspace, and this command names none`,
|
|
308
|
+
'session commands name their workspace — pass --workspace <id>',
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The failure a session-lane command reports, rewritten, or `null` to report
|
|
314
|
+
* it as it is. The original error rides along as `cause`, with its details.
|
|
315
|
+
*
|
|
316
|
+
* A key gets the key message too, not only the hint: an API client's own
|
|
317
|
+
* message names a revoked key or a missing capability, which contradicts the
|
|
318
|
+
* hint on the line below it. `sessionLaneRefusal` refuses keys before any
|
|
319
|
+
* request, so this branch is a guard for a failure raised on some other path.
|
|
320
|
+
*
|
|
321
|
+
* A person refused with the service's generic FORBIDDEN keeps the service
|
|
322
|
+
* message and gets the person hint, because the generic hint says "use a
|
|
323
|
+
* different key". A `CliError` is left alone: the CLI worded it for this
|
|
324
|
+
* command already — `credentialFailure` names the capability when it knows it.
|
|
325
|
+
*/
|
|
326
|
+
export function sessionLaneFailure(
|
|
327
|
+
meta: SessionLaneMeta,
|
|
328
|
+
token: string,
|
|
329
|
+
err: unknown,
|
|
330
|
+
): CliError | null {
|
|
331
|
+
const failure = toEnvelope(err)
|
|
332
|
+
const hint = sessionLaneHint(meta, token, failure.code)
|
|
333
|
+
if (!hint) return null
|
|
334
|
+
if (token.startsWith('sk-')) {
|
|
335
|
+
return new CliError(sessionLaneKeyMessage(meta), { code: failure.code, hint, cause: err })
|
|
336
|
+
}
|
|
337
|
+
if (err instanceof CliError) return null
|
|
338
|
+
return new CliError(failure.message, { code: failure.code, hint, cause: err })
|
|
339
|
+
}
|
package/src/flag-help.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
31
31
|
'dry-run': 'derive and print the write path without building anything',
|
|
32
32
|
'plan-id': 'reuse this mutation plan id instead of minting one, so a retry names the same artifact',
|
|
33
33
|
'binding-id': 'reuse this Binding id instead of minting one, so a retry names the same artifact',
|
|
34
|
-
reason: 'reason recorded
|
|
34
|
+
reason: 'reason recorded with the deployment transition, request or approval decision',
|
|
35
35
|
'no-activate': 'review the write path but leave it switched off',
|
|
36
36
|
role: 'organization role the capability is held by',
|
|
37
37
|
add: 'comma-separated property API names to add to the current set',
|
|
@@ -87,7 +87,10 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
87
87
|
input: "JSON object of the declared inputs — inline ('{\"key\": 1}') or @file.json",
|
|
88
88
|
// The primary key alone. A subject-bound workflow takes exactly one, so the
|
|
89
89
|
// flag spends no characters on the `{"pk": …}` the route models it as.
|
|
90
|
-
subject: 'primary key of the object a subject-bound workflow
|
|
90
|
+
subject: 'primary key of the object a subject-bound workflow or Action acts on',
|
|
91
|
+
// Action requests.
|
|
92
|
+
'subject-version': 'record version of --subject the request expects, so a stale read is refused',
|
|
93
|
+
'idempotency-key': 'reuse a key to retry a submit and get the same request back; generated if omitted',
|
|
91
94
|
'no-promote': 'publish the version without moving the live pointer',
|
|
92
95
|
'skip-build-check': 'deploy even when the build output is older than the source',
|
|
93
96
|
'no-wait': 'return as soon as the run is queued instead of waiting for it to finish',
|
|
@@ -95,6 +98,9 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
95
98
|
// between their working copy and what is deployed.
|
|
96
99
|
dev: 'run the file served by an `automation dev` session instead of a deployed version',
|
|
97
100
|
draft: 'act on your saved draft rather than a published version',
|
|
101
|
+
// Chat Apps
|
|
102
|
+
accent: 'presentation accent: blue, violet, green, amber, rose, teal or slate',
|
|
103
|
+
validate: 'report what blocks publishing, without publishing',
|
|
98
104
|
|
|
99
105
|
// Agents
|
|
100
106
|
name: 'display name; defaults to the slug',
|
|
@@ -150,6 +156,7 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
150
156
|
|
|
151
157
|
// Knowledge
|
|
152
158
|
description: 'one-line description stored on the resource',
|
|
159
|
+
'display-name': 'human-readable name shown beside the identifier',
|
|
153
160
|
'top-k': 'how many chunks to return (default 10)',
|
|
154
161
|
// A DISTANCE cutoff, not the similarity the output column shows — the query
|
|
155
162
|
// keeps chunks whose cosine distance is below it, so a HIGHER value is more
|
|
@@ -161,6 +168,17 @@ export const FLAG_HELP: Readonly<Record<string, string>> = {
|
|
|
161
168
|
strategy:
|
|
162
169
|
'text extraction to use per file: auto (default), text, or ocr; ocr needs an OCR engine on the base',
|
|
163
170
|
|
|
171
|
+
// Media Sets
|
|
172
|
+
markings: 'comma-separated security markings added to each uploaded item, on top of the set default',
|
|
173
|
+
ocr: 'extract text with OCR instead of reading the text layer — for scans and images',
|
|
174
|
+
'skip-embed': 'cut passages but do not embed them; search needs embeddings',
|
|
175
|
+
watch: 'poll until nothing is pending (up to 10 minutes), then print the final state',
|
|
176
|
+
'skip-existing':
|
|
177
|
+
'upload only paths the set does not already hold, so a re-run after a partial failure adds no versions. '
|
|
178
|
+
+ 'A path held by an item above your clearance is not seen, and is uploaded again',
|
|
179
|
+
'max-distance': 'maximum cosine distance a passage may have; higher is more permissive',
|
|
180
|
+
permissions: 'comma-separated: read, write, manage, read_sensitive (default read; read is always included)',
|
|
181
|
+
|
|
164
182
|
// Auth and setup
|
|
165
183
|
// "key", not "workspace key": `login` verifies an organization key too, and
|
|
166
184
|
// naming only one kind here was the last place the CLI still implied that a
|
|
@@ -196,6 +214,7 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
|
196
214
|
'expect-revision': 'hash',
|
|
197
215
|
notes: 'text',
|
|
198
216
|
description: 'text',
|
|
217
|
+
'display-name': 'text',
|
|
199
218
|
strategy: 'auto|text|ocr',
|
|
200
219
|
from: '-|path',
|
|
201
220
|
'password-from': '-|path',
|
|
@@ -212,6 +231,9 @@ const PLACEHOLDER: Readonly<Record<string, string>> = {
|
|
|
212
231
|
'new-capability': 'enabled|disabled',
|
|
213
232
|
'top-k': 'n',
|
|
214
233
|
threshold: '0-2',
|
|
234
|
+
markings: 'a,b',
|
|
235
|
+
'max-distance': '0-2',
|
|
236
|
+
permissions: 'read,write',
|
|
215
237
|
template: 'non-negative|percentage|status-enum|code-length',
|
|
216
238
|
}
|
|
217
239
|
|
package/src/forge/touches.ts
CHANGED
|
@@ -167,6 +167,8 @@ export const MCP_READ_TOOLS = new Set([
|
|
|
167
167
|
// what the copilot's tool search ranks on, and changing it to suit this
|
|
168
168
|
// regex would cost reach on the surface people actually use.
|
|
169
169
|
'workflow_list', 'workflow_inspect', 'workflow_resources', 'workflow_runs',
|
|
170
|
+
// Shows the steps about to be built as placeholders; writes nothing.
|
|
171
|
+
'workflow_plan',
|
|
170
172
|
// Runs one step of a draft and changes no part of the definition. Same
|
|
171
173
|
// call as `test_agent` two lines up, and for the same reason: an authoring
|
|
172
174
|
// loop tests a step repeatedly, and each one would jerk the pane.
|
package/src/harness.ts
CHANGED
|
@@ -59,9 +59,11 @@ and it cannot drift, because both are projected from the same table.
|
|
|
59
59
|
Two fields decide whether a call can work at all:
|
|
60
60
|
|
|
61
61
|
- \`authLane\` — \`api-key\` reaches the command with \`sk-ws-\`/\`sk-org-\` or a
|
|
62
|
-
session. \`session\`
|
|
63
|
-
|
|
64
|
-
\`
|
|
62
|
+
session. \`session\` acts as a person and needs a person's session, not an API
|
|
63
|
+
key: set \`FRONTERA_TOKEN\` to the session token and \`FRONTERA_API_URL\` (or
|
|
64
|
+
pass \`--api-url\`). The CLI refuses an \`sk-ws-\`/\`sk-org-\` key on those
|
|
65
|
+
before sending anything; \`frontera login\` stores keys only, so it cannot
|
|
66
|
+
supply the session.
|
|
65
67
|
- \`available\` — \`false\` means the verb is named but unbuilt, and
|
|
66
68
|
\`unavailableReason\` says why. Pick another route rather than retrying.
|
|
67
69
|
|
package/src/main.ts
CHANGED
|
@@ -3,11 +3,11 @@ import { parseArgs } from './args'
|
|
|
3
3
|
import { resolveCredential } from './config'
|
|
4
4
|
import { findProjectRoot } from './context'
|
|
5
5
|
import { CliError, UsageError } from './errors'
|
|
6
|
-
import { EXIT } from './exit'
|
|
6
|
+
import { EXIT, sessionLaneFailure, sessionLaneRefusal } from './exit'
|
|
7
7
|
import { resolveOrganization, setSelectedOrganization } from './organization'
|
|
8
8
|
import { createOutput, type OutputMode } from './output'
|
|
9
9
|
import { readProject } from './project'
|
|
10
|
-
import { commandScope } from './scopes'
|
|
10
|
+
import { commandScope, unaddressedWorkspaceRefusal } from './scopes'
|
|
11
11
|
import {
|
|
12
12
|
aliasesFor,
|
|
13
13
|
describeCommand,
|
|
@@ -142,6 +142,8 @@ async function main(): Promise<number> {
|
|
|
142
142
|
|
|
143
143
|
const mode: OutputMode = flags.json === true ? 'json' : 'human'
|
|
144
144
|
const output = createOutput(mode, { quiet: flags.quiet === true })
|
|
145
|
+
// Read by the failure path: which credential was refused decides the hint.
|
|
146
|
+
let token = ''
|
|
145
147
|
|
|
146
148
|
try {
|
|
147
149
|
// Reserved commands fail BEFORE anything else — no credential is
|
|
@@ -208,6 +210,7 @@ async function main(): Promise<number> {
|
|
|
208
210
|
},
|
|
209
211
|
{ cwd },
|
|
210
212
|
)
|
|
213
|
+
token = credential.token
|
|
211
214
|
|
|
212
215
|
const workspaceFlag = typeof flags.workspace === 'string' ? flags.workspace.trim() : undefined
|
|
213
216
|
const orgFlag = typeof flags.org === 'string' ? flags.org.trim() : undefined
|
|
@@ -241,41 +244,21 @@ async function main(): Promise<number> {
|
|
|
241
244
|
)
|
|
242
245
|
}
|
|
243
246
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
!command.meta.offline
|
|
261
|
-
&& !workspaceFlag
|
|
262
|
-
// A missing argument outranks a missing workspace, for the reason the
|
|
263
|
-
// project gate above gives: `skill pull` with no skill named has a
|
|
264
|
-
// better error waiting inside the command, and it names the command
|
|
265
|
-
// that produces the value. Let it be raised.
|
|
266
|
-
&& !missingArg
|
|
267
|
-
&& commandScope(command.meta) === 'workspace'
|
|
268
|
-
&& credential.token.startsWith('sk-org-')
|
|
269
|
-
) {
|
|
270
|
-
throw new CliError(
|
|
271
|
-
`\`${label}\` acts inside a workspace, `
|
|
272
|
-
+ 'and this organization key names none',
|
|
273
|
-
{
|
|
274
|
-
code: 'FORBIDDEN',
|
|
275
|
-
hint: 'pass --workspace <id> (see `frontera workspace list`), '
|
|
276
|
-
+ 'or use a workspace key (sk-ws-…) created for the workspace you mean',
|
|
277
|
-
},
|
|
278
|
-
)
|
|
247
|
+
// A missing argument outranks a missing workspace, for the reason the
|
|
248
|
+
// project gate above gives: `skill pull` with no skill named has a better
|
|
249
|
+
// error waiting inside the command, and it names the command that
|
|
250
|
+
// produces the value. Let it be raised.
|
|
251
|
+
if (!command.meta.offline) {
|
|
252
|
+
const refusal = sessionLaneRefusal({
|
|
253
|
+
meta: command.meta,
|
|
254
|
+
token: credential.token,
|
|
255
|
+
workspace: workspaceFlag,
|
|
256
|
+
missingArg: missingArg !== undefined,
|
|
257
|
+
})
|
|
258
|
+
?? (missingArg
|
|
259
|
+
? null
|
|
260
|
+
: unaddressedWorkspaceRefusal({ meta: command.meta, token: credential.token, workspace: workspaceFlag }))
|
|
261
|
+
if (refusal) throw refusal
|
|
279
262
|
}
|
|
280
263
|
|
|
281
264
|
/**
|
|
@@ -319,7 +302,9 @@ async function main(): Promise<number> {
|
|
|
319
302
|
output.render(result.data, () => result.text)
|
|
320
303
|
return EXIT.OK
|
|
321
304
|
} catch (err) {
|
|
322
|
-
|
|
305
|
+
// A key on a session-lane command: one recovery for every such noun,
|
|
306
|
+
// rather than each API client rewording its own 401/403.
|
|
307
|
+
return output.fail(sessionLaneFailure(command.meta, token, err) ?? err)
|
|
323
308
|
}
|
|
324
309
|
}
|
|
325
310
|
|
package/src/scopes.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CliError } from './errors'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Which grain each noun acts at — stated once.
|
|
3
5
|
*
|
|
@@ -54,3 +56,44 @@ export function commandScope(meta: {
|
|
|
54
56
|
export function orgScopedServiceNouns(): string[] {
|
|
55
57
|
return [...ORG_SCOPED_NOUNS].filter((n) => n !== 'auth' && n !== 'kit').sort()
|
|
56
58
|
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The refusal for an organization key that named no workspace on a
|
|
62
|
+
* workspace-scoped command, or `null` to let the request go.
|
|
63
|
+
*
|
|
64
|
+
* An organization key that named no workspace cannot run a
|
|
65
|
+
* workspace-scoped command, so it is refused rather than warned.
|
|
66
|
+
*
|
|
67
|
+
* This was a note, and a note was not enough. The request still went out,
|
|
68
|
+
* ran at organization scope, matched nothing, and `agent list` printed
|
|
69
|
+
* "No agents in this workspace." and exited 0 — for a request that had
|
|
70
|
+
* never looked at a workspace. Every sibling already refused (`app` and
|
|
71
|
+
* `pack` with 400, `skill` with 403, `knowledge` and `secret` with advice);
|
|
72
|
+
* `agent` was the one that answered a plausible lie with a success code.
|
|
73
|
+
*
|
|
74
|
+
* The wording is `knowledge`'s, because both routes out are real: an
|
|
75
|
+
* organization key names a workspace per invocation, a workspace key
|
|
76
|
+
* carries one permanently.
|
|
77
|
+
*/
|
|
78
|
+
export function unaddressedWorkspaceRefusal(opts: {
|
|
79
|
+
meta: { noun: string; verb: string; scope?: 'workspace' | 'organization' }
|
|
80
|
+
token: string
|
|
81
|
+
workspace: string | undefined
|
|
82
|
+
}): CliError | null {
|
|
83
|
+
const { meta, token, workspace } = opts
|
|
84
|
+
if (workspace || commandScope(meta) !== 'workspace' || !token.startsWith('sk-org-')) return null
|
|
85
|
+
return new CliError(
|
|
86
|
+
`\`${commandLabel(meta)}\` acts inside a workspace, `
|
|
87
|
+
+ 'and this organization key names none',
|
|
88
|
+
{
|
|
89
|
+
code: 'FORBIDDEN',
|
|
90
|
+
hint: 'pass --workspace <id> (see `frontera workspace list`), '
|
|
91
|
+
+ 'or use a workspace key (sk-ws-…) created for the workspace you mean',
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `noun verb`, as a reader typed it. */
|
|
97
|
+
export function commandLabel(meta: { noun: string; verb: string }): string {
|
|
98
|
+
return `${meta.noun}${meta.verb ? ` ${meta.verb}` : ''}`
|
|
99
|
+
}
|
package/src/template.ts
CHANGED
|
@@ -247,9 +247,10 @@ createFronteraApp(<App />, { providers: [blueprintProvider] })
|
|
|
247
247
|
import { useFronteraApp } from '@frontera-sdk/core/create-frontera-app'
|
|
248
248
|
|
|
249
249
|
/**
|
|
250
|
-
* The object type this page reads.
|
|
250
|
+
* The object type this page reads. Optional.
|
|
251
251
|
*
|
|
252
|
-
*
|
|
252
|
+
* Left empty, the App builds, deploys and runs without Blueprint data and sends
|
|
253
|
+
* no queries. To read records, \`frontera blueprint list\` shows what this workspace exposes; put an API name
|
|
253
254
|
* here — \`Shipment\`, \`LoanApplication\`, whatever the deployment models — and
|
|
254
255
|
* \`frontera blueprint get <apiName>\` lists its properties.
|
|
255
256
|
*
|
|
@@ -275,8 +276,9 @@ export default function App() {
|
|
|
275
276
|
|
|
276
277
|
{OBJECT_TYPE === '' ? (
|
|
277
278
|
<div className="rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground">
|
|
278
|
-
|
|
279
|
-
<code className="font-mono text-foreground">
|
|
279
|
+
Your App is running. Blueprint data is optional: to read records, set{' '}
|
|
280
|
+
<code className="font-mono text-foreground">OBJECT_TYPE</code> in{' '}
|
|
281
|
+
<code className="font-mono text-foreground">src/App.tsx</code>. Run{' '}
|
|
280
282
|
<code className="font-mono text-foreground">frontera blueprint list</code> to see what
|
|
281
283
|
this workspace exposes.
|
|
282
284
|
</div>
|
|
@@ -349,9 +349,9 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
349
349
|
emptyHint: 'Clear the search, or widen the filter.',
|
|
350
350
|
errorTitle: 'Could not load records',
|
|
351
351
|
retry: 'Try again',
|
|
352
|
-
unconfiguredTitle: '
|
|
352
|
+
unconfiguredTitle: 'Your App is running',
|
|
353
353
|
unconfiguredBody:
|
|
354
|
-
'
|
|
354
|
+
'Blueprint data is optional. To list records here, run \`frontera blueprint list\` and set STARTER_OBJECT_TYPE in src/lib/blueprint/starter.ts — or replace this page with your own.',
|
|
355
355
|
previous: 'Previous',
|
|
356
356
|
next: 'Next',
|
|
357
357
|
range: (from: number, to: number, total: number | null) =>
|
|
@@ -361,8 +361,10 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
361
361
|
`,
|
|
362
362
|
|
|
363
363
|
'src/lib/blueprint/starter.ts': `/**
|
|
364
|
-
* The object type the starter page reads.
|
|
364
|
+
* The object type the starter page reads. Optional.
|
|
365
365
|
*
|
|
366
|
+
* Left empty, the App builds, deploys and runs without Blueprint data; the
|
|
367
|
+
* starter page shows a welcome panel and sends no queries. To list records,
|
|
366
368
|
* \`frontera blueprint list\` shows what this workspace exposes; put an API name
|
|
367
369
|
* here — \`Shipment\`, \`LoanApplication\`, whatever the deployment models — and
|
|
368
370
|
* \`frontera blueprint get <apiName>\` lists its properties.
|
|
@@ -1004,7 +1006,9 @@ frontera app dev
|
|
|
1004
1006
|
with real, short-lived Blueprint access. \`bun run dev\` alone starts Next
|
|
1005
1007
|
without a session — useful for pure layout work, not for data.
|
|
1006
1008
|
|
|
1007
|
-
## Point it at your data
|
|
1009
|
+
## Point it at your data (optional)
|
|
1010
|
+
|
|
1011
|
+
The App runs without Blueprint data. To have the starter page list records:
|
|
1008
1012
|
|
|
1009
1013
|
\`\`\`bash
|
|
1010
1014
|
frontera blueprint list
|