@foldrun/core 9.0.0 → 9.2.0

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 (65) hide show
  1. package/dist/src/a2a-client.d.ts +165 -0
  2. package/dist/src/a2a-client.js +440 -0
  3. package/dist/src/a2a-publish.d.ts +37 -0
  4. package/dist/src/a2a-publish.js +116 -0
  5. package/dist/src/a2a-remotes.d.ts +73 -0
  6. package/dist/src/a2a-remotes.js +192 -0
  7. package/dist/src/a2a-tools.d.ts +148 -0
  8. package/dist/src/a2a-tools.js +833 -0
  9. package/dist/src/api-tools.js +15 -10
  10. package/dist/src/approvals.d.ts +18 -10
  11. package/dist/src/approvals.js +51 -1
  12. package/dist/src/browser-pod.js +4 -0
  13. package/dist/src/catalog.d.ts +14 -0
  14. package/dist/src/catalog.js +28 -0
  15. package/dist/src/completions.js +8 -8
  16. package/dist/src/cron.d.ts +9 -0
  17. package/dist/src/cron.js +20 -0
  18. package/dist/src/frontmatter-fields.js +3 -1
  19. package/dist/src/frontmatter-schema.d.ts +5 -1
  20. package/dist/src/frontmatter-schema.js +44 -1
  21. package/dist/src/limits.js +22 -4
  22. package/dist/src/notify.d.ts +6 -3
  23. package/dist/src/notify.js +25 -12
  24. package/dist/src/package.d.ts +5 -1
  25. package/dist/src/package.js +5 -3
  26. package/dist/src/run-container.d.ts +10 -1
  27. package/dist/src/run-container.js +17 -0
  28. package/dist/src/runner.d.ts +10 -0
  29. package/dist/src/runner.js +163 -16
  30. package/dist/src/scan.js +123 -5
  31. package/dist/src/secrets.d.ts +122 -10
  32. package/dist/src/secrets.js +291 -27
  33. package/dist/src/starter.js +5 -1
  34. package/dist/src/store.d.ts +34 -0
  35. package/dist/src/store.js +28 -7
  36. package/dist/src/subagents.d.ts +16 -0
  37. package/dist/src/subagents.js +23 -2
  38. package/dist/src/triggers.js +2 -2
  39. package/dist/src/webhook.d.ts +19 -3
  40. package/dist/src/webhook.js +14 -5
  41. package/package.json +4 -1
  42. package/src/a2a-client.ts +517 -0
  43. package/src/a2a-publish.ts +120 -0
  44. package/src/a2a-remotes.ts +225 -0
  45. package/src/a2a-tools.ts +919 -0
  46. package/src/api-tools.ts +20 -8
  47. package/src/approvals.ts +55 -1
  48. package/src/browser-pod.ts +3 -0
  49. package/src/catalog.ts +27 -0
  50. package/src/completions.ts +8 -8
  51. package/src/cron.ts +19 -0
  52. package/src/frontmatter-fields.ts +3 -1
  53. package/src/frontmatter-schema.ts +42 -2
  54. package/src/limits.ts +22 -4
  55. package/src/notify.ts +23 -12
  56. package/src/package.ts +5 -3
  57. package/src/run-container.ts +22 -0
  58. package/src/runner.ts +157 -13
  59. package/src/scan.ts +146 -6
  60. package/src/secrets.ts +319 -33
  61. package/src/starter.ts +5 -1
  62. package/src/store.ts +47 -2
  63. package/src/subagents.ts +29 -2
  64. package/src/triggers.ts +2 -2
  65. package/src/webhook.ts +22 -5
@@ -31,12 +31,27 @@ interface StoredSecret {
31
31
  * these sites alone. Metadata, not a value, so it is stored in the clear
32
32
  * beside the ciphertext and survives the value being set again. */
33
33
  sites?: string[];
34
+ /** An ACCOUNT secret limited to these workspaces (GitHub's "selected
35
+ * repositories" for an organisation secret). Absent: every workspace may
36
+ * use it, as before. Every other workspace is answered as if it were not
37
+ * set (getSecret). Metadata like `sites`: in the clear, and in the seal
38
+ * of a bound record. Never on a workspace's own record. */
39
+ workspaces?: string[];
34
40
  /** 2: sealed with the record's place as AAD — account, scope, name, kind,
35
- * key and sites (vaultAad). Absent: the older, unbound form. */
41
+ * key, sites and, when limited, workspaces (vaultAad). Absent: the older,
42
+ * unbound form. */
36
43
  v?: 2;
37
44
  }
38
45
  type SecretsFile = Record<string, StoredSecret>;
39
46
  declare const writeBound: () => boolean;
47
+ /**
48
+ * An exclusive lock on `file` for a read-change-write, across processes and
49
+ * pods: `<file>.lock` opened O_EXCL (NFS 4.1 and EFS honour it), taken over
50
+ * when older than LOCK_STALE_MS (a holder that died), waited for up to
51
+ * LOCK_WAIT_MS, then `busy` is thrown. Returns the release. The vault's,
52
+ * and any other small state file several runs write (a2a-tools' handles).
53
+ */
54
+ export declare function lockFileExclusive(file: string, busy?: string): () => void;
40
55
  /**
41
56
  * Change one vault file safely: `change` gets what is stored now and returns
42
57
  * the new contents, or null to leave the file untouched. Returns what is
@@ -44,7 +59,30 @@ declare const writeBound: () => boolean;
44
59
  */
45
60
  export declare function mutateVaultFile(file: string, change: (all: SecretsFile) => SecretsFile | null): SecretsFile;
46
61
  export declare const SECRET_NAME: RegExp;
47
- export declare function setSecret(tenant: string, name: string, value: string, workspace?: string, kind?: SecretKind): void;
62
+ /**
63
+ * How a `signing_secret:` is shown in an answer, a log line or a delivery
64
+ * record: the name it holds, and only a name (30 Oct 2026). It is meant to
65
+ * name a vault entry, but nothing stopped the value itself being written
66
+ * there, and notify's test send echoed it back to the dashboard as "signed
67
+ * with <it>". Anything that is not a secret name is described, never
68
+ * repeated (foldrun check flags it as secret/signing-secret-value).
69
+ */
70
+ export declare function signingSecretShown(name: string): string;
71
+ /**
72
+ * Which workspaces an ACCOUNT secret may be used in, given with the value so
73
+ * both are sealed in one locked write (30 Oct 2026): a new secret created
74
+ * and limited in two calls was open to every workspace between them, and
75
+ * stayed open if the second failed. `workspaces` undefined keeps what the
76
+ * record has; null opens it to every workspace; a list limits it.
77
+ * `exists` is asked about each name under the vault lock, so a workspace
78
+ * deleted meanwhile is not granted (the delete takes its name off every
79
+ * list under the same lock — retargetSecretWorkspaces).
80
+ */
81
+ export interface SecretLimit {
82
+ workspaces?: string[] | null;
83
+ exists?: (workspace: string) => boolean;
84
+ }
85
+ export declare function setSecret(tenant: string, name: string, value: string, workspace?: string, kind?: SecretKind, limit?: SecretLimit): void;
48
86
  /**
49
87
  * Bind a secret to sites, or unbind it (an empty list). The secret must
50
88
  * exist in that scope. Sites are `api.example.com` or `*.example.com` — no
@@ -71,11 +109,61 @@ export declare function decryptValue(tenant: string, rec: {
71
109
  data: string;
72
110
  k?: "tenant";
73
111
  } | undefined): string | null;
74
- /** Nearest wins: a workspace secret shadows an account one of the same name. */
112
+ /** Nearest wins: a workspace secret shadows an account one of the same name.
113
+ * An account secret limited to some workspaces (setSecretWorkspaces) is
114
+ * absent for every other — the answer a secret that was never set gets, on
115
+ * every path that resolves one for a workspace: runs, tool tests, notify,
116
+ * the egress proxy, MCP servers. Without a workspace it is the account's
117
+ * own use (its model key, its mail sender), which no list limits. */
75
118
  export declare function getSecret(tenant: string, name: string, workspace?: string): {
76
119
  value: string;
77
120
  scope: SecretScope;
78
121
  } | null;
122
+ /** May the ACCOUNT's secret `name` be used for this workspace? True when it
123
+ * is not limited (or not set); for callers that read the account's copy on
124
+ * a workspace's behalf without getSecret's workspace-first lookup — the
125
+ * account's mail sender for a workspace's notifications (notify.ts). */
126
+ export declare function accountSecretUsableIn(tenant: string, name: string, workspace: string): boolean;
127
+ /**
128
+ * Limit an account secret to some workspaces, or lift the limit (null).
129
+ * An empty list means no workspace may use it. Returns the list as stored,
130
+ * sorted, or null.
131
+ *
132
+ * `exists` is asked about each name under the vault lock (the API passes
133
+ * "is there such a workspace"), so it cannot grant a name a delete has just
134
+ * taken off every list. `ifWorkspaces`, when given, is the list the caller
135
+ * saw (null: every workspace): a list changed since is refused (409), so
136
+ * two admins editing it at once do not silently overwrite each other.
137
+ *
138
+ * The record is sealed (v2) whatever it was, whatever
139
+ * FOLDRUN_VAULT_WRITE_BOUND says: a list in the clear on an unbound record
140
+ * could be stripped on disk to open the secret to every workspace; sealed,
141
+ * a stripped list makes it unreadable. Every reader since 29 Oct 2026 reads v2.
142
+ */
143
+ export declare function setSecretWorkspaces(tenant: string, name: string, workspaces: string[] | null, opts?: {
144
+ exists?: (workspace: string) => boolean;
145
+ ifWorkspaces?: string[] | null;
146
+ }): string[] | null;
147
+ /**
148
+ * A workspace is being renamed (to: its new name) or deleted (to: null):
149
+ * every account secret limited to it follows. Without this a rename quietly
150
+ * cut the workspace off its secrets, and a delete left the name on the list
151
+ * for the next workspace given it.
152
+ *
153
+ * `failed` names the records that hold the name but will not open with this
154
+ * install's keys: they are left exactly as they are, so the old name is
155
+ * STILL on their list — a key restored later would bring the grant back.
156
+ * The caller must not report the rename or delete as clean while any are
157
+ * left (the API refuses it, naming them: set them again, or delete them).
158
+ *
159
+ * Names, not ids: a grant naming a workspace's immutable id would survive a
160
+ * rename untouched and could never pass to a new workspace given an old
161
+ * name — the stronger design, not built yet (30 Oct 2026).
162
+ */
163
+ export declare function retargetSecretWorkspaces(tenant: string, from: string, to: string | null): {
164
+ changed: string[];
165
+ failed: string[];
166
+ };
79
167
  export declare function deleteSecret(tenant: string, name: string, workspace?: string): void;
80
168
  export interface SecretEntry {
81
169
  name: string;
@@ -89,6 +177,8 @@ export interface SecretEntry {
89
177
  /** The sites it is bound to — the proxy fills it for these and no other,
90
178
  * and it never enters a sandbox. Absent when unbound. */
91
179
  sites?: string[];
180
+ /** An account secret limited to these workspaces; absent: every one. */
181
+ workspaces?: string[];
92
182
  }
93
183
  export declare function listSecrets(tenant: string, workspace?: string): SecretEntry[];
94
184
  export declare function resolveSecrets(tenant: string, names: string[], workspace?: string): {
@@ -154,7 +244,7 @@ export interface OAuth2Config {
154
244
  export declare const OAUTH_REFRESH_TRIES = 3;
155
245
  /** Wait before try n (1-based retry): 1s, 3s — FOLDRUN_OAUTH_RETRY_MS scales it for tests. */
156
246
  export declare function oauthRetryDelayMs(n: number): number;
157
- export declare function setOAuth2Secret(tenant: string, name: string, config: OAuth2Config, workspace?: string): void;
247
+ export declare function setOAuth2Secret(tenant: string, name: string, config: OAuth2Config, workspace?: string, limit?: SecretLimit): void;
158
248
  export declare function isOAuth2Value(value: string): boolean;
159
249
  /**
160
250
  * The cache key for a live token: SHA-256 over the kind, the account and the
@@ -193,6 +283,28 @@ ctx?: {
193
283
  * the stored value with the new refresh token. */
194
284
  onRotate?: (name: string, usedValue: string, next: string) => unknown;
195
285
  }): Promise<Record<string, string>>;
286
+ /**
287
+ * materializeSecrets for a tool that holds secrets for a whole step and
288
+ * makes them live per call (the API tools, the a2a tools), on a host with
289
+ * the vault.
290
+ *
291
+ * Two things the step's copy of a recipe cannot do alone. A provider that
292
+ * rotates refresh tokens has spent the refresh token the step started with —
293
+ * the runner materialised it at step start, and any refresh since spent its
294
+ * successor — so an oauth2 recipe is read again from the vault as it is now
295
+ * (and `env` updated in place, for the next call). And it is made live in
296
+ * the account's scope, as the runner and the egress proxy do: the cached
297
+ * access token is keyed by account, and a rotation is written back to the
298
+ * vault by the compare-and-set above. Refreshing the starting recipe outside
299
+ * that scope missed the cache and sent a spent refresh token: invalid_grant.
300
+ *
301
+ * No `scope` (inside a sandbox, where there is no vault and the values
302
+ * crossed already made live): materializeSecrets as it was.
303
+ */
304
+ export declare function materializeCurrent(env: Record<string, string>, scope?: {
305
+ tenant: string;
306
+ workspace?: string;
307
+ } | null): Promise<Record<string, string>>;
196
308
  export interface ServiceAccountConfig {
197
309
  token_url: string;
198
310
  issuer: string;
@@ -205,8 +317,8 @@ export interface ServiceAccountConfig {
205
317
  * a Workspace admin grants it. Absent, the account acts as itself. */
206
318
  subject?: string;
207
319
  }
208
- export declare function setServiceAccountSecret(tenant: string, name: string, config: ServiceAccountConfig, workspace?: string): void;
209
- export declare function setFileSecret(tenant: string, name: string, content: string, workspace?: string): void;
320
+ export declare function setServiceAccountSecret(tenant: string, name: string, config: ServiceAccountConfig, workspace?: string, limit?: SecretLimit): void;
321
+ export declare function setFileSecret(tenant: string, name: string, content: string, workspace?: string, limit?: SecretLimit): void;
210
322
  export declare function isFileValue(value: string): boolean;
211
323
  export declare function fileContent(value: string): string;
212
324
  export interface SshConfig {
@@ -217,7 +329,7 @@ export interface SshConfig {
217
329
  private_key?: string;
218
330
  password?: string;
219
331
  }
220
- export declare function setSshSecret(tenant: string, name: string, config: SshConfig, workspace?: string): void;
332
+ export declare function setSshSecret(tenant: string, name: string, config: SshConfig, workspace?: string, limit?: SecretLimit): void;
221
333
  export declare function isSshValue(value: string): boolean;
222
334
  export declare function sshConfigOf(value: string): SshConfig;
223
335
  /** A client certificate for mutual TLS: presented by the egress proxy, on its
@@ -232,14 +344,14 @@ export interface ClientCertConfig {
232
344
  passphrase?: string;
233
345
  export?: boolean;
234
346
  }
235
- export declare function setClientCertSecret(tenant: string, name: string, config: ClientCertConfig, workspace?: string): void;
347
+ export declare function setClientCertSecret(tenant: string, name: string, config: ClientCertConfig, workspace?: string, limit?: SecretLimit): void;
236
348
  /** A keyless cloud identity: no credential at all, only the audience the
237
349
  * platform's signed tokens are for. A step that declares it gets a token
238
350
  * file the sandbox driver keeps fresh through the egress proxy. */
239
351
  export interface IdentityConfig {
240
352
  audience: string;
241
353
  }
242
- export declare function setIdentitySecret(tenant: string, name: string, config: IdentityConfig, workspace?: string): void;
354
+ export declare function setIdentitySecret(tenant: string, name: string, config: IdentityConfig, workspace?: string, limit?: SecretLimit): void;
243
355
  export declare function isIdentityValue(value: string): boolean;
244
356
  export declare function identityOf(value: string): IdentityConfig;
245
357
  export declare function isClientCertValue(value: string): boolean;
@@ -260,7 +372,7 @@ export interface ApiConfig {
260
372
  /** `Authorization: Basic …` from a username and password (RFC 7617 §2):
261
373
  * UTF-8, no colon in the username, no control characters in either. */
262
374
  export declare function basicAuthorization(username: string, password: string): string;
263
- export declare function setApiSecret(tenant: string, name: string, config: ApiConfig, workspace?: string): void;
375
+ export declare function setApiSecret(tenant: string, name: string, config: ApiConfig, workspace?: string, limit?: SecretLimit): void;
264
376
  export declare function isApiValue(value: string): boolean;
265
377
  export declare function apiConfigOf(value: string): ApiConfig;
266
378
  /**