@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontera-sdk/automation",
3
- "version": "1.45.13",
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.13",
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 { MAX_INPUT_BYTES, sanitizeInputsSchema, validateInputValue } from './inputs'
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(['type', 'required', 'default', 'description', 'enum'])
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
+ }