@replayio/app-building 1.33.0 → 1.34.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 +128 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -63,46 +63,146 @@ Secrets are never passed directly to the container or agent. Instead:
|
|
|
63
63
|
2. At startup, the container fetches global secrets from Infisical for internal use (clone token, agent API key).
|
|
64
64
|
3. A **secrets server** (`127.0.0.1:9119`) runs inside the container, accessible only locally. It fetches secrets live from Infisical on every request — no caching.
|
|
65
65
|
4. The agent process runs with a **restricted environment** — only `ANTHROPIC_API_KEY` (required for the Claude CLI) is present.
|
|
66
|
-
5.
|
|
66
|
+
5. Any time a secret is needed, the caller invokes `exec-secrets`:
|
|
67
67
|
|
|
68
|
-
```bash
|
|
69
|
-
exec-secrets
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
```bash
|
|
69
|
+
exec-secrets <SECRET1> [SECRET2 …] -- <target> [args …]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The secrets server spawns the target with the named secrets in its
|
|
73
|
+
environment and **redacts those secret values from the output**.
|
|
72
74
|
|
|
73
|
-
|
|
75
|
+
`exec-secrets` is invoked recursively. The agent's own shell has no secrets,
|
|
76
|
+
so when it runs an app script — `npm run test`, `npm run deploy`, a seed
|
|
77
|
+
script, a migration — that script calls `exec-secrets` itself for each
|
|
78
|
+
operation that needs a secret. One agent task can produce dozens of
|
|
79
|
+
`exec-secrets` invocations from inside scripts it never directly typed.
|
|
74
80
|
|
|
75
|
-
|
|
81
|
+
### Three commands you'll see
|
|
82
|
+
|
|
83
|
+
| Command | Purpose |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `exec-secrets <SECRETS…> -- <target> [args…]` | Run `<target>` with the named secrets injected, output redacted. |
|
|
86
|
+
| `list-secrets` | Print the secret names the container can resolve. With an allowlist configured, also prints the allowed targets. |
|
|
87
|
+
| `set-branch-secret <NAME> <value>` | Store a new branch-scoped secret in Infisical (e.g. `DATABASE_URL` after provisioning Neon). Rejected if `<value>` has already appeared in logs. |
|
|
88
|
+
|
|
89
|
+
In **unrestricted mode** (no allowlist), `<target>` is any binary the
|
|
90
|
+
container has installed — `curl`, `psql`, `npx netlify`, etc. In
|
|
91
|
+
**restricted mode** (allowlist configured), `<target>` must be one of the
|
|
92
|
+
allowlist entry names; see Allowlist mode below.
|
|
76
93
|
|
|
77
94
|
### Allowlist mode
|
|
78
95
|
|
|
79
|
-
Set `ContainerConfig.secretAllowlist` to restrict
|
|
96
|
+
Set `ContainerConfig.secretAllowlist` to restrict the set of secret-using
|
|
97
|
+
operations available in the container. With an allowlist configured, every
|
|
98
|
+
`exec-secrets` call — whether issued directly by the agent or by an app
|
|
99
|
+
script the agent runs (`npm run test`, deploy scripts, seed scripts,
|
|
100
|
+
migrations) — must name an entry; calls naming an arbitrary binary are
|
|
101
|
+
rejected.
|
|
102
|
+
|
|
103
|
+
Each entry is `{ name, helpString, shellCommand }`. The shellCommand body
|
|
104
|
+
runs under `sh -c`; positional args supplied after the target become `$1`,
|
|
105
|
+
`$2`, … and the named secrets are present in the environment.
|
|
106
|
+
|
|
107
|
+
#### Design principle: one entry per verb
|
|
108
|
+
|
|
109
|
+
Each entry should encode **one specific operation**. The `shellCommand`
|
|
110
|
+
hardcodes URL, method, file path, and any other fixed structure; positional
|
|
111
|
+
args carry only the data the operation needs. The caller supplies "what to
|
|
112
|
+
operate on", never "what to do".
|
|
113
|
+
|
|
114
|
+
#### Good entries
|
|
80
115
|
|
|
81
116
|
```ts
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
secretAllowlist: [
|
|
118
|
+
{
|
|
119
|
+
name: "neon-create-branch",
|
|
120
|
+
helpString: "Create a Neon branch. Args: <project_id> <branch_name>",
|
|
121
|
+
shellCommand:
|
|
122
|
+
'curl -fsS -X POST "https://console.neon.tech/api/v2/projects/$1/branches" ' +
|
|
123
|
+
'-H "Authorization: Bearer $NEON_API_KEY" ' +
|
|
124
|
+
'-H "Content-Type: application/json" ' +
|
|
125
|
+
'-d "{\\"branch\\":{\\"name\\":\\"$2\\"}}"',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: "neon-delete-branch",
|
|
129
|
+
helpString: "Delete a Neon branch. Args: <project_id> <branch_id>",
|
|
130
|
+
shellCommand:
|
|
131
|
+
'curl -fsS -X DELETE "https://console.neon.tech/api/v2/projects/$1/branches/$2" ' +
|
|
132
|
+
'-H "Authorization: Bearer $NEON_API_KEY"',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "netlify-deploy-prod",
|
|
136
|
+
helpString: "Deploy the current build to production. No args.",
|
|
137
|
+
shellCommand:
|
|
138
|
+
'npx netlify deploy --prod --auth "$NETLIFY_AUTH_TOKEN" --site "$NETLIFY_SITE_ID"',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "replay-upload-all",
|
|
142
|
+
helpString: "Upload all pending Replay recordings. No args.",
|
|
143
|
+
shellCommand: 'npx replayio upload --all --api-key "$RECORD_REPLAY_API_KEY"',
|
|
144
|
+
},
|
|
145
|
+
]
|
|
97
146
|
```
|
|
98
147
|
|
|
99
|
-
|
|
148
|
+
Each of these:
|
|
149
|
+
|
|
150
|
+
- Pins the URL, method, and headers — the caller can't redirect a Neon API
|
|
151
|
+
token at a different host or use it for an operation that wasn't allowed.
|
|
152
|
+
- Pins the binary and its flags — Netlify / Replay invocations always carry
|
|
153
|
+
the right auth and the intended verb.
|
|
154
|
+
- Takes data only — branch names, project IDs, etc.
|
|
100
155
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
156
|
+
#### Anti-patterns
|
|
157
|
+
|
|
158
|
+
Don't pass `"$@"` through to a primitive tool. These look like allowlist
|
|
159
|
+
entries but they're not constraining anything:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// BAD — caller can curl any URL with the Neon token attached.
|
|
163
|
+
{ name: "curl", shellCommand: 'curl "$@"' }
|
|
164
|
+
// BAD — caller can run arbitrary SQL, including `\!` shell escapes.
|
|
165
|
+
{ name: "psql", shellCommand: 'psql "$@"' }
|
|
166
|
+
// BAD — caller can run arbitrary JS with every secret in env.
|
|
167
|
+
{ name: "node", shellCommand: 'node "$@"' }
|
|
168
|
+
// BAD — sh/bash with -c is identical to "run anything".
|
|
169
|
+
{ name: "shell", shellCommand: 'sh -c "$1"' }
|
|
170
|
+
```
|
|
104
171
|
|
|
105
|
-
|
|
172
|
+
If the same upstream API has ten operations the app needs, write ten
|
|
173
|
+
entries — each hardcoding URL + method + headers, each taking only the
|
|
174
|
+
data fields the operation requires.
|
|
175
|
+
|
|
176
|
+
#### In-repo scripts aren't a security boundary
|
|
177
|
+
|
|
178
|
+
The allowlist only constrains operations whose semantics live **outside the
|
|
179
|
+
repo** — upstream HTTP APIs, third-party CLIs invoked with fixed flags. For
|
|
180
|
+
anything that runs code the agent wrote (`npx tsx scripts/seed-db.ts`,
|
|
181
|
+
`node scripts/migrate.js`, `npm run <anything>`), the agent controls the
|
|
182
|
+
file contents and can do whatever it likes with the secrets in env.
|
|
183
|
+
Pinning a path doesn't help — the agent can edit the file.
|
|
184
|
+
|
|
185
|
+
So: do **not** add allowlist entries that exist to run an in-repo script.
|
|
186
|
+
Instead, write narrow API-level entries (`neon-create-branch`,
|
|
187
|
+
`netlify-env-set`, …) and have the in-repo script call `exec-secrets` for
|
|
188
|
+
each of those verbs as needed. The allowlist then describes the set of
|
|
189
|
+
operations the system supports, and the agent's code composes them.
|
|
190
|
+
|
|
191
|
+
#### Behavior when configured
|
|
192
|
+
|
|
193
|
+
- `list-secrets` returns `{ secrets, allowlist }` — `secrets` are the names
|
|
194
|
+
available before `--`; `allowlist` is `name — helpString` per entry,
|
|
195
|
+
available as targets after `--`.
|
|
196
|
+
- `exec-secrets <SECRET1> [SECRET2 …] -- <name> [args…]`: named secrets are
|
|
197
|
+
injected into env (and only those values are redacted from output);
|
|
198
|
+
`<name>` must match an entry; `[args…]` become `$1`, `$2`, … inside the
|
|
199
|
+
entry's `shellCommand`. `$0` is `"exec-secrets"`.
|
|
200
|
+
- Targets not in the allowlist are rejected with `Unknown allowlist entry`.
|
|
201
|
+
|
|
202
|
+
`secretAllowlist` is serialized into the container as `SECRET_ALLOWLIST_JSON`;
|
|
203
|
+
the secrets server parses it on startup. To swap or clear the allowlist on
|
|
204
|
+
a running container without restarting, POST to `/reconfigure` (see
|
|
205
|
+
Container HTTP API).
|
|
106
206
|
|
|
107
207
|
## Exported API
|
|
108
208
|
|