@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 +54 -16
- package/dist/container.d.ts +2 -0
- package/dist/container.js +2 -0
- package/dist/secrets.d.ts +5 -0
- package/dist/secrets.js +40 -0
- package/package.json +1 -1
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
|
|
27
|
+
// Load orchestration vars from .env, then get Infisical credentials
|
|
29
28
|
const orchestrationVars = loadDotEnv("/path/to/project");
|
|
30
|
-
const infisicalConfig = getInfisicalConfig(orchestrationVars);
|
|
31
|
-
|
|
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:
|
|
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` (
|
|
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
|
-
| `
|
|
127
|
-
| `
|
|
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:
|
|
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:
|
|
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",
|
package/dist/container.d.ts
CHANGED
|
@@ -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,
|