@frontera-sdk/automation 1.45.13 → 1.45.14
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 +2 -2
- package/src/index.ts +12 -1
- package/src/inputs.ts +53 -1
- package/src/manifest.ts +13 -1
- package/src/types.ts +82 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontera-sdk/automation",
|
|
3
|
-
"version": "1.45.
|
|
3
|
+
"version": "1.45.14",
|
|
4
4
|
"description": "Author Frontera automations: manifest, triggers and the typed handler contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontera",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"typescript": "^5.9.3"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@frontera-sdk/blueprint": "1.45.
|
|
45
|
+
"@frontera-sdk/blueprint": "1.45.14",
|
|
46
46
|
"cron-parser": "^5.0.6"
|
|
47
47
|
}
|
|
48
48
|
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,17 @@ export {
|
|
|
11
11
|
} from './messages'
|
|
12
12
|
export { createTestContext } from './testing'
|
|
13
13
|
export type { TestCall, TestContext, TestContextOptions } from './testing'
|
|
14
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
MAX_INPUT_BYTES,
|
|
16
|
+
redactedInputKeys,
|
|
17
|
+
sanitizeInputsSchema,
|
|
18
|
+
validateInputValue,
|
|
19
|
+
} from './inputs'
|
|
15
20
|
export type { InputValidation } from './inputs'
|
|
21
|
+
export {
|
|
22
|
+
EVENT_TRIGGER_SOURCES,
|
|
23
|
+
TICKETED_TRIGGER_SOURCES,
|
|
24
|
+
isEventTriggerSource,
|
|
25
|
+
isTicketedTriggerSource,
|
|
26
|
+
} from './types'
|
|
16
27
|
export type * from './types'
|
package/src/inputs.ts
CHANGED
|
@@ -42,7 +42,14 @@ const INPUT_TYPES = new Set<InputFieldSpec['type']>(['string', 'number', 'boolea
|
|
|
42
42
|
* Anything else there is a warning, same philosophy as `manifest.ts`'s
|
|
43
43
|
* top-level unknown-key warning: a typo like `requred` should be visible,
|
|
44
44
|
* but a field a newer SDK added must not fail an older validator's deploy. */
|
|
45
|
-
const INPUT_SPEC_KEYS = new Set([
|
|
45
|
+
const INPUT_SPEC_KEYS = new Set([
|
|
46
|
+
'type',
|
|
47
|
+
'required',
|
|
48
|
+
'default',
|
|
49
|
+
'description',
|
|
50
|
+
'enum',
|
|
51
|
+
'redact',
|
|
52
|
+
])
|
|
46
53
|
|
|
47
54
|
/**
|
|
48
55
|
* Serialized cap on a run's input object — the same 64KB the service enforces
|
|
@@ -105,6 +112,7 @@ export function checkInputFieldSpec(key: string, raw: unknown): InputFieldCheck
|
|
|
105
112
|
default?: unknown
|
|
106
113
|
enum?: unknown
|
|
107
114
|
description?: unknown
|
|
115
|
+
redact?: unknown
|
|
108
116
|
}
|
|
109
117
|
|
|
110
118
|
if (spec.description !== undefined && typeof spec.description !== 'string') {
|
|
@@ -129,6 +137,29 @@ export function checkInputFieldSpec(key: string, raw: unknown): InputFieldCheck
|
|
|
129
137
|
errors.push(`input "${key}": required and default are mutually exclusive — a default always satisfies required`)
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
// Same `=== true` predicate as `required`, for the same reason: the runtime
|
|
141
|
+
// treats only a literal `true` as active, so a truthy check here would let
|
|
142
|
+
// `redact: 1` deploy clean and then mask nothing.
|
|
143
|
+
if (spec.redact !== undefined && typeof spec.redact !== 'boolean') {
|
|
144
|
+
errors.push(`input "${key}": redact must be a boolean`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Both of these publish the value the flag claims to hide, so they are
|
|
148
|
+
// refused rather than warned about: a manifest that declares them is a
|
|
149
|
+
// masking guarantee that was never going to hold.
|
|
150
|
+
if (spec.redact === true && spec.default !== undefined) {
|
|
151
|
+
errors.push(
|
|
152
|
+
`input "${key}": redact and default are mutually exclusive — a default is published in the `
|
|
153
|
+
+ 'version manifest, so the value would be readable there',
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
if (spec.redact === true && spec.enum !== undefined) {
|
|
157
|
+
errors.push(
|
|
158
|
+
`input "${key}": redact and enum are mutually exclusive — an enum publishes every allowed `
|
|
159
|
+
+ 'value in the version manifest',
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
132
163
|
let enumOk = true
|
|
133
164
|
if (spec.enum !== undefined) {
|
|
134
165
|
if (t !== 'string' && t !== 'number') {
|
|
@@ -242,3 +273,24 @@ export function sanitizeInputsSchema(inputs: unknown): InputsSchema | undefined
|
|
|
242
273
|
}
|
|
243
274
|
return inputs as InputsSchema
|
|
244
275
|
}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The input names a version declared `redact: true` on.
|
|
280
|
+
*
|
|
281
|
+
* One implementation, so the service, the Console and anything else answer
|
|
282
|
+
* "which fields are masked" the same way — the same argument that keeps
|
|
283
|
+
* `checkInputFieldSpec` shared between deploy and runtime.
|
|
284
|
+
*
|
|
285
|
+
* FAIL CLOSED IS THE CALLER'S JOB, and it matters: `sanitizeInputsSchema`
|
|
286
|
+
* returns `undefined` for the WHOLE schema when any single field is malformed,
|
|
287
|
+
* which a caller deriving this list from its result would read as "nothing is
|
|
288
|
+
* redacted". A caller holding a non-empty raw `inputs` whose sanitized form is
|
|
289
|
+
* `undefined` must mask everything rather than nothing.
|
|
290
|
+
*/
|
|
291
|
+
export function redactedInputKeys(schema: InputsSchema | undefined): string[] {
|
|
292
|
+
if (!schema) return []
|
|
293
|
+
return Object.entries(schema)
|
|
294
|
+
.filter(([, spec]) => spec?.redact === true)
|
|
295
|
+
.map(([key]) => key)
|
|
296
|
+
}
|
package/src/manifest.ts
CHANGED
|
@@ -300,13 +300,25 @@ export function validateManifest(input: unknown): ValidationResult {
|
|
|
300
300
|
const agentInputs = m.inputs
|
|
301
301
|
if (agentInputs && typeof agentInputs === 'object' && !Array.isArray(agentInputs)) {
|
|
302
302
|
for (const [key, raw] of Object.entries(agentInputs as Record<string, unknown>)) {
|
|
303
|
-
const spec = raw as { description?: unknown } | null
|
|
303
|
+
const spec = raw as { description?: unknown; redact?: unknown } | null
|
|
304
304
|
if (typeof spec?.description !== 'string' || spec.description.trim().length === 0) {
|
|
305
305
|
errors.push(
|
|
306
306
|
`input "${key}" needs a description: trigger { agent: true } publishes every input as `
|
|
307
307
|
+ 'a tool argument, and an agent cannot fill an argument it has no description for.',
|
|
308
308
|
)
|
|
309
309
|
}
|
|
310
|
+
// An error, not a warning, and refused here where the author still has
|
|
311
|
+
// the file open. On the agent path the MODEL produces this value as
|
|
312
|
+
// tool-call arguments: it is in the conversation and in that
|
|
313
|
+
// conversation's trace before a run row exists to mask. Masking the run
|
|
314
|
+
// row would advertise a guarantee this path cannot keep.
|
|
315
|
+
if (spec?.redact === true) {
|
|
316
|
+
errors.push(
|
|
317
|
+
`input "${key}": redact cannot be used with trigger { agent: true } — the agent `
|
|
318
|
+
+ 'supplies this value as a tool argument, so it is already in the conversation and '
|
|
319
|
+
+ 'its trace before the run exists.',
|
|
320
|
+
)
|
|
321
|
+
}
|
|
310
322
|
}
|
|
311
323
|
}
|
|
312
324
|
}
|
package/src/types.ts
CHANGED
|
@@ -84,6 +84,45 @@ export interface InputFieldSpec {
|
|
|
84
84
|
description?: string
|
|
85
85
|
/** Allowed values — string and number types only. */
|
|
86
86
|
enum?: readonly (string | number)[]
|
|
87
|
+
/**
|
|
88
|
+
* Mask this field's VALUE wherever a person reads the run.
|
|
89
|
+
*
|
|
90
|
+
* What it changes: the run list, the run page and `frontera automation runs`
|
|
91
|
+
* show a placeholder instead of the value. What it does NOT change: the
|
|
92
|
+
* handler, every resumption and every retried attempt receive the real value,
|
|
93
|
+
* because the run row still holds it — this is a display control, not
|
|
94
|
+
* storage encryption and not an access control.
|
|
95
|
+
*
|
|
96
|
+
* What it CANNOT cover, stated here so the flag never reads as a promise it
|
|
97
|
+
* does not keep:
|
|
98
|
+
*
|
|
99
|
+
* - `ctx.log('…', { key: ctx.input.token })` — an author writing a value
|
|
100
|
+
* into a step detail publishes it, and nothing here can intercept that.
|
|
101
|
+
* - a redacted value the author TRANSFORMS before using it. The agent
|
|
102
|
+
* transcript on the run page is masked by exact occurrence, so a value
|
|
103
|
+
* interpolated into a prompt — or quoted back in the reply — is replaced.
|
|
104
|
+
* A value upper-cased, truncated or reformatted first no longer matches
|
|
105
|
+
* and is not found. Exact match is what can be done without guessing at
|
|
106
|
+
* substrings; the alternative, withholding transcripts entirely for any
|
|
107
|
+
* run with a redacted input, would take the review surface away from
|
|
108
|
+
* exactly the runs that most need reviewing.
|
|
109
|
+
* - `default` and `enum`, which are published in the version manifest and in
|
|
110
|
+
* any tool schema built from it. Declaring either alongside `redact` is
|
|
111
|
+
* refused at deploy for exactly that reason.
|
|
112
|
+
* - the run's own `result` and error message. A handler that returns the
|
|
113
|
+
* value — `return { note: ctx.input.customer_note }` — or throws an error
|
|
114
|
+
* quoting it publishes it on the same run page, unmasked, next to the
|
|
115
|
+
* masked input it came from. Only the INPUT is masked; what the handler
|
|
116
|
+
* chooses to emit is the handler's decision.
|
|
117
|
+
* - anything already recorded. Versions are append-only and the mask is
|
|
118
|
+
* frozen onto each run when it starts, so adding `redact` masks future
|
|
119
|
+
* runs and never rewrites history.
|
|
120
|
+
*
|
|
121
|
+
* A credential still belongs in a `secret:` grant, whose value never enters
|
|
122
|
+
* this process at all. `redact` is for the ordinary personal or commercial
|
|
123
|
+
* detail a run legitimately takes and a bystander has no reason to read.
|
|
124
|
+
*/
|
|
125
|
+
redact?: boolean
|
|
87
126
|
}
|
|
88
127
|
|
|
89
128
|
export type InputsSchema = Record<string, InputFieldSpec>
|
|
@@ -461,3 +500,46 @@ export interface AutomationDescriptor {
|
|
|
461
500
|
readonly manifest: ResolvedAutomationManifest
|
|
462
501
|
readonly handler: AutomationHandler
|
|
463
502
|
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* How a run came to exist, named once so the two sides of the invoke event
|
|
506
|
+
* cannot drift.
|
|
507
|
+
*
|
|
508
|
+
* This is not decoration. The service publishes `source` on the invoke event,
|
|
509
|
+
* the RUNNER resolves it back and posts it to the open-run call, and the
|
|
510
|
+
* service then refuses an invocation ticket arriving under a source that does
|
|
511
|
+
* not expect one. So a source the service knows and the runner does not is not
|
|
512
|
+
* a mis-filed run — it is no run at all: the ticket rides along, the open-run
|
|
513
|
+
* call 400s, and the caller waits forever on a request that never became
|
|
514
|
+
* anything. That is precisely how the App lane shipped broken.
|
|
515
|
+
*
|
|
516
|
+
* Both packages depend on this one, so the list lives here rather than being
|
|
517
|
+
* spelled out in each. Adding a source means adding it here, and the two
|
|
518
|
+
* consumers pick it up by construction.
|
|
519
|
+
*
|
|
520
|
+
* `cron` is deliberately absent: it is what the runner INFERS when the event
|
|
521
|
+
* names no source at all, so it is never carried on an event.
|
|
522
|
+
*/
|
|
523
|
+
export const EVENT_TRIGGER_SOURCES = ['manual', 'rehearsal', 'agent', 'app'] as const
|
|
524
|
+
export type EventTriggerSource = (typeof EVENT_TRIGGER_SOURCES)[number]
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* The sources whose runs MUST arrive with an invocation ticket.
|
|
528
|
+
*
|
|
529
|
+
* A run under one of these has a caller whose identity exists only on the
|
|
530
|
+
* ticket, so a missing one is refused rather than opened unattributed. A ticket
|
|
531
|
+
* under any other source is refused too — it means the event was tampered with
|
|
532
|
+
* or two payloads got mixed.
|
|
533
|
+
*/
|
|
534
|
+
export const TICKETED_TRIGGER_SOURCES = ['agent', 'app'] as const
|
|
535
|
+
export type TicketedTriggerSource = (typeof TICKETED_TRIGGER_SOURCES)[number]
|
|
536
|
+
|
|
537
|
+
export function isEventTriggerSource(value: unknown): value is EventTriggerSource {
|
|
538
|
+
return typeof value === 'string'
|
|
539
|
+
&& (EVENT_TRIGGER_SOURCES as readonly string[]).includes(value)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function isTicketedTriggerSource(value: unknown): value is TicketedTriggerSource {
|
|
543
|
+
return typeof value === 'string'
|
|
544
|
+
&& (TICKETED_TRIGGER_SOURCES as readonly string[]).includes(value)
|
|
545
|
+
}
|