@replayio/app-building 1.15.0 → 1.17.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.
package/README.md CHANGED
@@ -15,7 +15,6 @@ import {
15
15
  loadDotEnv,
16
16
  FileContainerRegistry,
17
17
  getInfisicalConfig,
18
- resolveContainerSecrets,
19
18
  createMachine,
20
19
  destroyMachine,
21
20
  type ContainerConfig,
@@ -25,14 +24,22 @@ import {
25
24
  httpOptsFor,
26
25
  } from "@replayio/app-building";
27
26
 
28
- // Load orchestration vars from .env, then fetch build secrets from Infisical
27
+ // Load orchestration vars from .env, then get Infisical credentials
29
28
  const orchestrationVars = loadDotEnv("/path/to/project");
30
- const infisicalConfig = getInfisicalConfig(orchestrationVars);
31
- const containerSecrets = await resolveContainerSecrets(infisicalConfig);
29
+ const infisicalConfig = await getInfisicalConfig(orchestrationVars);
30
+
31
+ // Only pass Infisical credentials to the container — not actual secrets.
32
+ // The container fetches secrets from Infisical at startup and manages them
33
+ // via an internal secrets server. The agent never has direct access to secrets.
34
+ const containerEnvVars: Record<string, string> = {
35
+ INFISICAL_TOKEN: infisicalConfig.token,
36
+ INFISICAL_PROJECT_ID: infisicalConfig.projectId,
37
+ INFISICAL_ENVIRONMENT: infisicalConfig.environment,
38
+ };
32
39
 
33
40
  const config: ContainerConfig = {
34
41
  projectRoot: "/path/to/project", // optional — only needed for local Docker operations
35
- envVars: containerSecrets,
42
+ envVars: containerEnvVars,
36
43
  registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
37
44
  flyToken: orchestrationVars.FLY_API_TOKEN,
38
45
  flyApp: orchestrationVars.FLY_APP_NAME,
@@ -53,13 +60,32 @@ const alive = await config.registry.findAlive();
53
60
  await destroyMachine(config.flyApp, config.flyToken, machineId, volumeId);
54
61
  ```
55
62
 
63
+ ## Secrets architecture
64
+
65
+ Secrets are never passed directly to the container or agent. Instead:
66
+
67
+ 1. The orchestration host passes **Infisical credentials** (token, project ID, environment) to the container.
68
+ 2. At startup, the container fetches all secrets from Infisical and stores them in memory.
69
+ 3. A **secrets server** (`127.0.0.1:9119`) runs inside the container, accessible only locally.
70
+ 4. The agent process runs with a **restricted environment** — only `ANTHROPIC_API_KEY` (required for the Claude CLI) is present.
71
+ 5. When the agent needs to run a command that requires secrets, it uses `exec-secrets`:
72
+
73
+ ```bash
74
+ exec-secrets NEON_API_KEY -- curl -s -H "Authorization: Bearer $NEON_API_KEY" https://...
75
+ exec-secrets NETLIFY_AUTH_TOKEN NETLIFY_ACCOUNT_SLUG -- netlify deploy --prod
76
+ ```
77
+
78
+ The secrets server spawns the command with the requested secrets in its environment and **redacts all secret values** from the output.
79
+
80
+ The agent can also run `list-secrets` to see which secrets are available, and `set-branch-secret` to store new branch-level secrets (e.g., `DATABASE_URL` created at deploy time). The server rejects values that have already appeared in logs.
81
+
56
82
  ## Exported API
57
83
 
58
84
  ### Domain objects
59
85
 
60
86
  | Export | Description |
61
87
  |---|---|
62
- | `ContainerConfig` | Interface bundling all external state: optional `projectRoot` (only needed for local Docker operations), `envVars` (build secrets from Infisical), `registry`, optional `flyToken`/`flyApp`/`imageRef`/`webhookUrl`/`webhookSecret`/`detached`/`initialPrompt`/`localPort`. See [Webhooks](#webhooks) and [Container lifecycle](#container-lifecycle) below. |
88
+ | `ContainerConfig` | Interface bundling all external state: optional `projectRoot` (only needed for local Docker operations), `envVars` (Infisical credentials), `registry`, optional `flyToken`/`flyApp`/`imageRef`/`webhookUrl`/`webhookSecret`/`detached`/`initialPrompt`/`localPort`/`absorbTasks`. See [Webhooks](#webhooks) and [Container lifecycle](#container-lifecycle) below. |
63
89
  | `RepoOptions` | Per-invocation git settings: `repoUrl`, `cloneBranch`, `pushBranch`. |
64
90
  | `ContainerRegistry` | Interface for container registry storage. Methods: `log`, `markStopped`, `clearStopped`, `getRecent`, `find`, `findAlive`. |
65
91
  | `FileContainerRegistry` | Built-in file-backed implementation of `ContainerRegistry`, backed by a `.jsonl` file. |
@@ -123,10 +149,11 @@ await destroyMachine(config.flyApp, config.flyToken, machineId, volumeId);
123
149
 
124
150
  | Export | Description |
125
151
  |---|---|
126
- | `getInfisicalConfig(envVars)` | Extract `InfisicalConfig` from env vars. Returns `null` if any required var is missing (enables fallback to raw `.env`). |
127
- | `resolveContainerSecrets(config)` | Fetch global build secrets from Infisical and merge with Infisical config vars. Returns a `Record<string, string>` suitable for `ContainerConfig.envVars`. |
152
+ | `infisicalLogin(clientId, clientSecret)` | Log in via Universal Auth, returns a short-lived access token. |
153
+ | `getInfisicalConfig(envVars)` | Extract `InfisicalConfig` from env vars and log in. Requires `INFISICAL_CLIENT_ID`, `INFISICAL_CLIENT_SECRET`, `INFISICAL_PROJECT_ID`, `INFISICAL_ENVIRONMENT`. |
128
154
  | `fetchGlobalSecrets(config)` | Fetch secrets from the `/global/` path. |
129
155
  | `fetchBranchSecrets(config, branch)` | Fetch secrets from `/branches/<branch>/`. |
156
+ | `createBranchSecret(config, branch, name, value)` | Create or update a secret in `/branches/<branch>/`. |
130
157
  | `fetchInfisicalSecrets(config, path)` | Raw fetch from any Infisical folder path. |
131
158
 
132
159
  **Types:** `InfisicalConfig`
@@ -135,13 +162,15 @@ await destroyMachine(config.flyApp, config.flyToken, machineId, volumeId);
135
162
 
136
163
  ```ts
137
164
  const orchestrationVars = loadDotEnv(projectRoot);
138
- const infisicalConfig = getInfisicalConfig(orchestrationVars);
139
- const containerSecrets = infisicalConfig
140
- ? await resolveContainerSecrets(infisicalConfig)
141
- : orchestrationVars; // fallback for local dev without Infisical
165
+ const infisicalConfig = await getInfisicalConfig(orchestrationVars);
142
166
 
167
+ // Pass only Infisical credentials to the container
143
168
  const config: ContainerConfig = {
144
- envVars: containerSecrets,
169
+ envVars: {
170
+ INFISICAL_TOKEN: infisicalConfig.token,
171
+ INFISICAL_PROJECT_ID: infisicalConfig.projectId,
172
+ INFISICAL_ENVIRONMENT: infisicalConfig.environment,
173
+ },
145
174
  flyToken: orchestrationVars.FLY_API_TOKEN,
146
175
  flyApp: orchestrationVars.FLY_APP_NAME,
147
176
  ...
@@ -186,6 +215,12 @@ A container stays running and accepts messages until it receives a **detach** or
186
215
  Without either signal, the container waits indefinitely for new messages — this is intentional
187
216
  so that interactive users can send follow-up messages at any time.
188
217
 
218
+ ### Task absorption
219
+
220
+ Set `config.absorbTasks = true` to have the container absorb task files from other containers
221
+ at startup. This is off by default. When enabled, the container scans `tasks/` for task files
222
+ belonging to other containers, merges their tasks into its own queue, and deletes the foreign files.
223
+
189
224
  ## Webhooks
190
225
 
191
226
  Set `webhookUrl` on `ContainerConfig` to receive real-time notifications of container activity. The container POSTs JSON to that URL on key events (no retries; failures are logged to stderr). Set `webhookSecret` to include a `Bearer` token in the `Authorization` header for authenticating webhook requests.
@@ -230,12 +265,15 @@ Every POST body has this shape:
230
265
 
231
266
  ```ts
232
267
  const orchestrationVars = loadDotEnv("/path/to/project");
233
- const infisicalConfig = getInfisicalConfig(orchestrationVars);
234
- const containerSecrets = await resolveContainerSecrets(infisicalConfig);
268
+ const infisicalConfig = await getInfisicalConfig(orchestrationVars);
235
269
 
236
270
  const config: ContainerConfig = {
237
271
  projectRoot: "/path/to/project",
238
- envVars: containerSecrets,
272
+ envVars: {
273
+ INFISICAL_TOKEN: infisicalConfig.token,
274
+ INFISICAL_PROJECT_ID: infisicalConfig.projectId,
275
+ INFISICAL_ENVIRONMENT: infisicalConfig.environment,
276
+ },
239
277
  registry: new FileContainerRegistry("/path/to/.container-registry.jsonl"),
240
278
  webhookUrl: "https://example.com/hooks/container-events",
241
279
  webhookSecret: "your-webhook-secret",
@@ -23,6 +23,8 @@ export interface ContainerConfig {
23
23
  initialPrompt?: string;
24
24
  /** Override the host port for local containers (default: auto-selected). */
25
25
  localPort?: number;
26
+ /** Absorb task files from other containers at startup. Default: false. */
27
+ absorbTasks?: boolean;
26
28
  }
27
29
  export interface RepoOptions {
28
30
  repoUrl: string;
package/dist/container.js CHANGED
@@ -119,6 +119,8 @@ export async function startContainer(config, repo) {
119
119
  extra.DETACHED = "1";
120
120
  if (config.initialPrompt)
121
121
  extra.INITIAL_PROMPT = config.initialPrompt;
122
+ if (config.absorbTasks)
123
+ extra.ABSORB_TASKS = "1";
122
124
  const containerEnv = buildContainerEnv(repo, config.envVars, extra);
123
125
  // Build docker run args
124
126
  const args = ["run", "-d", "--rm", "--name", containerName];
package/dist/secrets.d.ts CHANGED
@@ -21,6 +21,11 @@ export declare function fetchGlobalSecrets(config: InfisicalConfig): Promise<Rec
21
21
  * Fetch per-branch deployment secrets from `/branches/<branch>/`.
22
22
  */
23
23
  export declare function fetchBranchSecrets(config: InfisicalConfig, branch: string): Promise<Record<string, string>>;
24
+ /**
25
+ * Create or update a branch secret in Infisical.
26
+ * Uses POST to create, falls back to PATCH if it already exists.
27
+ */
28
+ export declare function createBranchSecret(config: InfisicalConfig, branch: string, name: string, value: string): Promise<void>;
24
29
  /**
25
30
  * Extract Infisical config from environment variables and log in.
26
31
  * Reads INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID,
package/dist/secrets.js CHANGED
@@ -59,6 +59,46 @@ export async function fetchGlobalSecrets(config) {
59
59
  export async function fetchBranchSecrets(config, branch) {
60
60
  return fetchInfisicalSecrets(config, `/branches/${branch}/`);
61
61
  }
62
+ /**
63
+ * Create or update a branch secret in Infisical.
64
+ * Uses POST to create, falls back to PATCH if it already exists.
65
+ */
66
+ export async function createBranchSecret(config, branch, name, value) {
67
+ const secretPath = `/branches/${branch}/`;
68
+ const url = `${INFISICAL_API_BASE}/api/v3/secrets/raw/${encodeURIComponent(name)}`;
69
+ const body = {
70
+ workspaceId: config.projectId,
71
+ environment: config.environment,
72
+ secretPath,
73
+ secretValue: value,
74
+ type: "shared",
75
+ };
76
+ const headers = {
77
+ Authorization: `Bearer ${config.token}`,
78
+ "Content-Type": "application/json",
79
+ };
80
+ const res = await fetch(url, {
81
+ method: "POST",
82
+ headers,
83
+ body: JSON.stringify(body),
84
+ });
85
+ if (res.ok)
86
+ return;
87
+ // If conflict (already exists), try PATCH to update
88
+ if (res.status === 400 || res.status === 409) {
89
+ const patchRes = await fetch(url, {
90
+ method: "PATCH",
91
+ headers,
92
+ body: JSON.stringify(body),
93
+ });
94
+ if (patchRes.ok)
95
+ return;
96
+ const text = await patchRes.text().catch(() => "");
97
+ throw new Error(`Infisical PATCH ${name} → ${patchRes.status}: ${text}`);
98
+ }
99
+ const text = await res.text().catch(() => "");
100
+ throw new Error(`Infisical POST ${name} → ${res.status}: ${text}`);
101
+ }
62
102
  /**
63
103
  * Extract Infisical config from environment variables and log in.
64
104
  * Reads INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayio/app-building",
3
- "version": "1.15.0",
3
+ "version": "1.17.0",
4
4
  "description": "Library for managing agentic app-building containers",
5
5
  "type": "module",
6
6
  "exports": {