@frontera-sdk/cli 1.45.10 → 1.45.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/cli",
3
- "version": "1.45.10",
3
+ "version": "1.45.12",
4
4
  "description": "The frontera CLI — scaffold, pull, save and deploy Frontera apps and automations.",
5
5
  "keywords": [
6
6
  "frontera",
@@ -38,8 +38,8 @@
38
38
  "build:release": "bun run scripts/build-release.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@frontera-sdk/automation": "1.45.9",
42
- "@frontera-sdk/core": "1.45.9",
41
+ "@frontera-sdk/automation": "1.45.12",
42
+ "@frontera-sdk/core": "1.45.12",
43
43
  "gray-matter": "^4.0.3",
44
44
  "yaml": "^2.9.0"
45
45
  },
@@ -26,6 +26,37 @@ export interface AutomationVersionSummary {
26
26
  createdAt: string
27
27
  }
28
28
 
29
+ /** One agent↔automation binding, as the bindings routes return it. */
30
+ export interface AgentAutomationBinding {
31
+ agentId: string
32
+ agentSlug: string
33
+ agentName: string
34
+ enabled: boolean
35
+ requiresApproval: boolean
36
+ waitSeconds: number
37
+ createdBy: string
38
+ createdAt: string
39
+ }
40
+
41
+ /**
42
+ * One binding seen from the agent's side.
43
+ *
44
+ * `projects` is the field worth reading: a binding can exist and grant nothing
45
+ * when the author has withdrawn `trigger: { agent: true }` from the live
46
+ * version, and a list without it reads as a working permission.
47
+ */
48
+ export interface AgentBindingView {
49
+ automationSlug: string
50
+ automationId: string
51
+ enabled: boolean
52
+ requiresApproval: boolean
53
+ waitSeconds: number
54
+ projects: boolean
55
+ inertReason: 'no_live_version' | 'not_agent_callable' | 'version_retired' | 'disabled' | null
56
+ createdBy: string
57
+ createdAt: string
58
+ }
59
+
29
60
  export interface AutomationDeployResult {
30
61
  automationId: string
31
62
  versionId: string
@@ -293,6 +324,44 @@ export class AutomationApi {
293
324
  )
294
325
  }
295
326
 
327
+ /**
328
+ * Which agents may run this automation from a conversation.
329
+ *
330
+ * Two locks have to be open before any of this matters: the automation's own
331
+ * manifest must declare `trigger: { agent: true }` (the author's half), and a
332
+ * binding must name the agent (the operator's half, these three calls).
333
+ */
334
+ bindings(slug: string): Promise<AgentAutomationBinding[]> {
335
+ return this.client.request<AgentAutomationBinding[]>(
336
+ `/v1/automations/${encodeURIComponent(slug)}/bindings`,
337
+ )
338
+ }
339
+
340
+ bind(
341
+ slug: string,
342
+ agentSlug: string,
343
+ options: { enabled?: boolean; requiresApproval?: boolean; waitSeconds?: number } = {},
344
+ ): Promise<AgentAutomationBinding> {
345
+ return this.client.request<AgentAutomationBinding>(
346
+ `/v1/automations/${encodeURIComponent(slug)}/bindings/${encodeURIComponent(agentSlug)}`,
347
+ { method: 'PUT', body: options },
348
+ )
349
+ }
350
+
351
+ /** The same question from the agent's side: what may this agent start? */
352
+ agentBindings(agentSlug: string): Promise<AgentBindingView[]> {
353
+ return this.client.request<AgentBindingView[]>(
354
+ `/v1/automations/agent-bindings/${encodeURIComponent(agentSlug)}`,
355
+ )
356
+ }
357
+
358
+ unbind(slug: string, agentSlug: string): Promise<{ unbound: boolean }> {
359
+ return this.client.request<{ unbound: boolean }>(
360
+ `/v1/automations/${encodeURIComponent(slug)}/bindings/${encodeURIComponent(agentSlug)}`,
361
+ { method: 'DELETE' },
362
+ )
363
+ }
364
+
296
365
  setEnabled(slug: string, enabled: boolean): Promise<{ slug: string; enabled: boolean }> {
297
366
  return this.client.request<{ slug: string; enabled: boolean }>(
298
367
  `/v1/automations/${encodeURIComponent(slug)}/enabled`,
@@ -0,0 +1,193 @@
1
+ /**
2
+ * `frontera automation bind|unbind|bindings` — the operator's half of making an
3
+ * automation callable from a conversation.
4
+ *
5
+ * The author's half is in the manifest (`trigger: { agent: true }`, plus a
6
+ * description of the automation and of every input) and is checked at deploy.
7
+ * These verbs cannot substitute for it: the service refuses a binding whose
8
+ * automation does not declare the flag, and the message says so.
9
+ *
10
+ * `bind` and `unbind` are separate verbs rather than `bind --off`, for the same
11
+ * reason `disable` is not `enable --false`: taking a permission away is done
12
+ * under pressure, and a flag is one more thing to get wrong.
13
+ */
14
+ import { AutomationApi } from '../../api/automation-api'
15
+ import { UsageError } from '../../errors'
16
+ import { table } from '../../table'
17
+ import { flagString, type Command, type CommandContext } from '../types'
18
+
19
+ function requireSlug(ctx: CommandContext): string {
20
+ const slug = ctx.positional[0]
21
+ if (!slug) {
22
+ throw new UsageError(
23
+ 'missing <slug>',
24
+ 'run `frontera automation list` — then pass the slug of the one you mean',
25
+ )
26
+ }
27
+ return slug
28
+ }
29
+
30
+ function requireAgent(ctx: CommandContext): string {
31
+ const agent = flagString(ctx, 'agent')
32
+ if (!agent) {
33
+ throw new UsageError(
34
+ 'missing --agent <agent-slug>',
35
+ 'a binding names exactly one agent — there is no "all agents" form on purpose',
36
+ )
37
+ }
38
+ return agent
39
+ }
40
+
41
+ const bind: Command = {
42
+ meta: {
43
+ noun: 'automation',
44
+ verb: 'bind',
45
+ args: [{ name: 'slug', required: true, description: 'automation slug' }],
46
+ flags: { agent: 'string', approval: 'string', wait: 'string' },
47
+ summary: 'Let one agent run this automation from a conversation',
48
+ examples: [
49
+ 'frontera automation bind reconcile-invoices --agent finance-analyst',
50
+ 'frontera automation bind reconcile-invoices --agent analyst --approval auto',
51
+ 'frontera automation bind reconcile-invoices --agent analyst --wait 120',
52
+ ],
53
+ },
54
+
55
+ async run(ctx) {
56
+ const slug = requireSlug(ctx)
57
+ const agent = requireAgent(ctx)
58
+
59
+ // `required` is the default at every layer, and it is spelled out here
60
+ // rather than left to the server's default so that `--approval` reads as a
61
+ // choice an operator made either way.
62
+ const approval = flagString(ctx, 'approval') ?? 'required'
63
+ if (approval !== 'required' && approval !== 'auto') {
64
+ throw new UsageError(
65
+ `--approval must be "required" or "auto", got "${approval}"`,
66
+ '`required` asks a human before each call; `auto` does not',
67
+ )
68
+ }
69
+
70
+ const rawWait = flagString(ctx, 'wait')
71
+ const waitSeconds = rawWait === undefined ? undefined : Number(rawWait)
72
+ if (waitSeconds !== undefined && !Number.isInteger(waitSeconds)) {
73
+ throw new UsageError(
74
+ `--wait must be a whole number of seconds, got "${rawWait}"`,
75
+ 'how long the agent waits for the run before it is told it is still going (5–300)',
76
+ )
77
+ }
78
+
79
+ const result = await new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId).bind(
80
+ slug,
81
+ agent,
82
+ { requiresApproval: approval === 'required', waitSeconds },
83
+ )
84
+
85
+ return {
86
+ data: result,
87
+ text:
88
+ `Bound ${slug} to ${result.agentSlug} — approval ${result.requiresApproval ? 'required' : 'not required'}, `
89
+ + `waits ${result.waitSeconds}s.`,
90
+ }
91
+ },
92
+ }
93
+
94
+ const unbind: Command = {
95
+ meta: {
96
+ noun: 'automation',
97
+ verb: 'unbind',
98
+ args: [{ name: 'slug', required: true, description: 'automation slug' }],
99
+ flags: { agent: 'string' },
100
+ summary: 'Stop one agent being able to run this automation',
101
+ examples: ['frontera automation unbind reconcile-invoices --agent finance-analyst'],
102
+ },
103
+
104
+ async run(ctx) {
105
+ const slug = requireSlug(ctx)
106
+ const agent = requireAgent(ctx)
107
+ await new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId).unbind(slug, agent)
108
+ return { data: { unbound: true }, text: `${agent} can no longer run ${slug}.` }
109
+ },
110
+ }
111
+
112
+ /**
113
+ * Both directions of the same question, because an operator asks it from both
114
+ * ends: the function owner asks who can run their code, and a security review
115
+ * asks what one agent can reach. Answering the second by walking every
116
+ * automation's bindings is the kind of fan-out that gets skipped.
117
+ */
118
+ const bindings: Command = {
119
+ meta: {
120
+ noun: 'automation',
121
+ verb: 'bindings',
122
+ args: [
123
+ {
124
+ name: 'slug',
125
+ required: false,
126
+ description: 'automation slug — omit it and pass --agent to ask from the agent side',
127
+ },
128
+ ],
129
+ flags: { agent: 'string' },
130
+ summary: 'List the agents that may run an automation, or what one agent may run',
131
+ examples: [
132
+ 'frontera automation bindings reconcile-invoices',
133
+ 'frontera automation bindings --agent finance-analyst',
134
+ ],
135
+ },
136
+
137
+ async run(ctx) {
138
+ const client = new AutomationApi(ctx.apiUrl, ctx.token, ctx.workspaceId)
139
+ const agent = flagString(ctx, 'agent')
140
+ const slug = ctx.positional[0]
141
+
142
+ if (!slug && !agent) {
143
+ throw new UsageError(
144
+ 'pass an automation slug, or --agent <agent-slug>',
145
+ 'the first lists who may run one automation; the second lists what one agent may run',
146
+ )
147
+ }
148
+
149
+ if (agent) {
150
+ const rows = await client.agentBindings(agent)
151
+ return {
152
+ data: rows,
153
+ text:
154
+ rows.length === 0
155
+ ? `${agent} cannot run any automation from a conversation.`
156
+ : table(
157
+ ['automation', 'grants a tool', 'approval', 'wait', 'why not'],
158
+ rows.map((b) => [
159
+ b.automationSlug,
160
+ // The load-bearing column. A binding that grants nothing
161
+ // still has a row, and a list that showed only the row would
162
+ // read as a working permission.
163
+ b.projects ? 'yes' : 'no',
164
+ b.requiresApproval ? 'required' : 'auto',
165
+ `${b.waitSeconds}s`,
166
+ b.inertReason ?? '',
167
+ ]),
168
+ ),
169
+ }
170
+ }
171
+
172
+ const rows = await client.bindings(slug!)
173
+ return {
174
+ data: rows,
175
+ text:
176
+ rows.length === 0
177
+ ? `No agent can run ${slug} from a conversation.`
178
+ : table(
179
+ ['agent', 'name', 'enabled', 'approval', 'wait', 'bound by'],
180
+ rows.map((b) => [
181
+ b.agentSlug,
182
+ b.agentName,
183
+ String(b.enabled),
184
+ b.requiresApproval ? 'required' : 'auto',
185
+ `${b.waitSeconds}s`,
186
+ b.createdBy,
187
+ ]),
188
+ ),
189
+ }
190
+ },
191
+ }
192
+
193
+ export const automationBindingCommands: Command[] = [bind, unbind, bindings]
@@ -11,6 +11,7 @@ import { automationInit } from './init'
11
11
  import { automationPull } from './pull'
12
12
  import { automationRun, automationRuns } from './run'
13
13
  import { automationDev } from './dev'
14
+ import { automationBindingCommands } from './bindings'
14
15
  import { buildAndExtract } from './build-entry'
15
16
 
16
17
  export { buildAndExtract } from './build-entry'
@@ -313,4 +314,5 @@ export const automationCommands: Command[] = [
313
314
  automationPull,
314
315
  automationInit,
315
316
  automationDev,
317
+ ...automationBindingCommands,
316
318
  ]
package/src/flag-help.ts CHANGED
@@ -13,6 +13,11 @@
13
13
  * fails if a command declares a flag this file does not describe.
14
14
  */
15
15
  export const FLAG_HELP: Readonly<Record<string, string>> = {
16
+ // Agent↔automation bindings. `agent` is deliberately singular: a binding
17
+ // names one agent, and there is no wildcard form.
18
+ agent: 'agent slug this binding is for — one agent, no wildcard',
19
+ approval: '"required" (default) asks a human before each call; "auto" does not',
20
+ wait: 'seconds the agent waits for the run before it is told the run is still going (5–300)',
16
21
  // Declarative Blueprint authoring — the file tree, and what applying it may do.
17
22
  prune: 'also remove artifacts that are on the draft but absent from the file tree',
18
23
  dataset: 'dataset name the object type reads through; the revision is resolved at apply time',