Skip to content

Commit 4d8716c

Browse files
committed
fix(sandboxes): spend the build budget after validation, conceal workspace ids on item routes, read manifest files in the CLI
1 parent 41957c9 commit 4d8716c

17 files changed

Lines changed: 449 additions & 71 deletions

File tree

apps/docs/content/docs/cli/reference.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2761,9 +2761,9 @@ sim sandboxes create [options]
27612761
| --- | --- | --- |
27622762
| `--name <value>` | Yes | Display name, unique within the workspace; 1 to 64 characters. |
27632763
| `--language <value>` | Yes | Dependency ecosystem: `javascript` installs from npm, `python` from PyPI. Accepted values: `javascript`, `python`. |
2764-
| `--dependencies <value...>` | No | Package specifiers installed into the sandbox, one per entry. Blank entries and `#` comments are ignored. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
2764+
| `--dependencies <value...>` | No | Package specifiers installed into the sandbox, one per entry. Blank entries and `#` comments are ignored. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
27652765
| `--cli-tools <value...>` | No | Pinned managed CLI ids installed into the sandbox, at most 10, no duplicates. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
2766-
| `--system-packages <value...>` | No | Debian packages installed into the sandbox image, one per entry. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
2766+
| `--system-packages <value...>` | No | Debian packages installed into the sandbox image, one per entry. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
27672767

27682768
</CommandTable>
27692769

@@ -2860,9 +2860,9 @@ sim sandboxes update <sandboxId> [options]
28602860
| --- | --- | --- |
28612861
| `--name <value>` | No | New display name, unique within the workspace; 1 to 64 characters. |
28622862
| `--language <value>` | No | Replacement dependency ecosystem. The whole spec is revalidated against it, so a Python dependency list does not survive a switch to JavaScript. Accepted values: `javascript`, `python`. |
2863-
| `--dependencies <value...>` | No | Replacement package list; replaces the whole list. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
2863+
| `--dependencies <value...>` | No | Replacement package list; replaces the whole list. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
28642864
| `--cli-tools <value...>` | No | Replacement managed CLI list; replaces the whole list. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
2865-
| `--system-packages <value...>` | No | Replacement Debian package list; replaces the whole list. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
2865+
| `--system-packages <value...>` | No | Replacement Debian package list; replaces the whole list. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
28662866

28672867
</CommandTable>
28682868

apps/docs/content/docs/cli/sandboxes.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ Create Sandbox (personal API key required)
2525
| --- | --- | --- |
2626
| `--name <value>` | Yes | Display name, unique within the workspace; 1 to 64 characters. |
2727
| `--language <value>` | Yes | Dependency ecosystem: `javascript` installs from npm, `python` from PyPI. Accepted values: `javascript`, `python`. |
28-
| `--dependencies <value...>` | No | Package specifiers installed into the sandbox, one per entry. Blank entries and `#` comments are ignored. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
28+
| `--dependencies <value...>` | No | Package specifiers installed into the sandbox, one per entry. Blank entries and `#` comments are ignored. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
2929
| `--cli-tools <value...>` | No | Pinned managed CLI ids installed into the sandbox, at most 10, no duplicates. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
30-
| `--system-packages <value...>` | No | Debian packages installed into the sandbox image, one per entry. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
30+
| `--system-packages <value...>` | No | Debian packages installed into the sandbox image, one per entry. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
3131

3232
</CommandTable>
3333

@@ -120,8 +120,8 @@ Update Sandbox (personal API key required)
120120
| --- | --- | --- |
121121
| `--name <value>` | No | New display name, unique within the workspace; 1 to 64 characters. |
122122
| `--language <value>` | No | Replacement dependency ecosystem. The whole spec is revalidated against it, so a Python dependency list does not survive a switch to JavaScript. Accepted values: `javascript`, `python`. |
123-
| `--dependencies <value...>` | No | Replacement package list; replaces the whole list. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
123+
| `--dependencies <value...>` | No | Replacement package list; replaces the whole list. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
124124
| `--cli-tools <value...>` | No | Replacement managed CLI list; replaces the whole list. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
125-
| `--system-packages <value...>` | No | Replacement Debian package list; replaces the whole list. (space-separated, or @path / @- with one value per line; @@value for a literal leading @). |
125+
| `--system-packages <value...>` | No | Replacement Debian package list; replaces the whole list. (space-separated, or @path / @- with one value per line, blank lines and # comments ignored; @@value for a literal leading @). |
126126

127127
</CommandTable>

apps/docs/openapi-v2-resources.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,7 +2143,7 @@
21432143
"post": {
21442144
"operationId": "createSandbox",
21452145
"summary": "Create Sandbox",
2146-
"description": "Create a sandbox and schedule its image build. The name must be unique within the workspace. A dependency or system-package entry the builder cannot accept is a `400` whose `error.details` names the field and the offending entries. Requires a workspace admin on a Max or Enterprise plan; a lower plan is refused with `403` and `error.details.code` `WORKSPACE_PLAN_CAPABILITY_REQUIRED`. Builds cost compute, so every write in a workspace draws on one shared budget, and a burst is refused with `429` and a `Retry-After` header. A workspace API key is rejected with `403`; use a personal API key.",
2146+
"description": "Create a sandbox. The name must be unique within the workspace. Where the deployment prebuilds dependency images, the build is scheduled and reported through `buildStatus`; a deployment that installs at run time, or a sandbox with nothing to install, has no build and reports `buildStatus: null`. A dependency or system-package entry the builder cannot accept is a `400` whose `error.details` names the field and the offending entries. Requires a workspace admin on a Max or Enterprise plan; a lower plan is refused with `403` and `error.details.code` `WORKSPACE_PLAN_CAPABILITY_REQUIRED`. Builds cost compute, so every write in a workspace draws on one shared budget, and a burst is refused with `429` and a `Retry-After` header. A workspace API key is rejected with `403`; use a personal API key.",
21472147
"tags": ["Sandboxes"],
21482148
"requestBody": {
21492149
"required": true,
@@ -2158,7 +2158,7 @@
21582158
},
21592159
"responses": {
21602160
"201": {
2161-
"description": "The sandbox was created and its build scheduled.",
2161+
"description": "The sandbox was created; a build is scheduled where the deployment prebuilds images.",
21622162
"headers": {
21632163
"X-RateLimit-Limit": {
21642164
"$ref": "#/components/headers/X-RateLimit-Limit"
@@ -2290,7 +2290,7 @@
22902290
"patch": {
22912291
"operationId": "updateSandbox",
22922292
"summary": "Update Sandbox",
2293-
"description": "Update the supplied sandbox fields and rebuild the image. Omitted fields retain their stored values; a supplied list replaces the whole list; names must remain unique within the workspace. Re-sending an unchanged spec after a failed build retries the build. Requires a workspace admin on a Max or Enterprise plan; a lower plan is refused with `403` and `error.details.code` `WORKSPACE_PLAN_CAPABILITY_REQUIRED`. Builds cost compute, so every write in a workspace draws on one shared budget, and a burst is refused with `429` and a `Retry-After` header. A workspace API key is rejected with `403`; use a personal API key.",
2293+
"description": "Update the supplied sandbox fields. Omitted fields retain their stored values; a supplied list replaces the whole list; names must remain unique within the workspace. Where the deployment prebuilds dependency images, a changed spec is rebuilt and re-sending an unchanged spec after a failed build retries it; a deployment that installs at run time, or a spec with nothing to install, has no build and reports `buildStatus: null`. Requires a workspace admin on a Max or Enterprise plan; a lower plan is refused with `403` and `error.details.code` `WORKSPACE_PLAN_CAPABILITY_REQUIRED`. Builds cost compute, so every write in a workspace draws on one shared budget, and a burst is refused with `429` and a `Retry-After` header. A workspace API key is rejected with `403`; use a personal API key.",
22942294
"tags": ["Sandboxes"],
22952295
"parameters": [
22962296
{
@@ -2373,7 +2373,7 @@
23732373
"delete": {
23742374
"operationId": "deleteSandbox",
23752375
"summary": "Delete Sandbox",
2376-
"description": "Delete a sandbox. Function blocks that still select it fail closed at run time until they are re-pointed, and the image is released once nothing else shares it. Requires a workspace admin on a Max or Enterprise plan; a lower plan is refused with `403` and `error.details.code` `WORKSPACE_PLAN_CAPABILITY_REQUIRED`. Builds cost compute, so every write in a workspace draws on one shared budget, and a burst is refused with `429` and a `Retry-After` header. A workspace API key is rejected with `403`; use a personal API key.",
2376+
"description": "Delete a sandbox. Function blocks that still select it fail closed at run time until they are re-pointed. Any prebuilt image is released once nothing else shares it. Requires a workspace admin on a Max or Enterprise plan; a lower plan is refused with `403` and `error.details.code` `WORKSPACE_PLAN_CAPABILITY_REQUIRED`. Builds cost compute, so every write in a workspace draws on one shared budget, and a burst is refused with `429` and a `Retry-After` header. A workspace API key is rejected with `403`; use a personal API key.",
23772377
"tags": ["Sandboxes"],
23782378
"parameters": [
23792379
{
@@ -7001,7 +7001,7 @@
70017001
"required": ["data"],
70027002
"additionalProperties": false,
70037003
"title": "Create sandbox response",
7004-
"description": "The created sandbox, with its build still pending.",
7004+
"description": "The created sandbox. `buildStatus` is `pending` while an image builds and `null` where nothing is built.",
70057005
"examples": [
70067006
{
70077007
"data": {
@@ -7155,7 +7155,7 @@
71557155
"required": ["data"],
71567156
"additionalProperties": false,
71577157
"title": "Update sandbox response",
7158-
"description": "The updated sandbox, with its rebuild pending.",
7158+
"description": "The updated sandbox. `buildStatus` is `pending` while an image rebuilds and `null` where nothing is built.",
71597159
"examples": [
71607160
{
71617161
"data": {

apps/sim/app/api/v2/sandboxes/[sandboxId]/route.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,18 @@ describe('/api/v2/sandboxes/[sandboxId]', () => {
166166
})
167167
})
168168

169+
it('answers a missing workspace as a missing sandbox, not as a missing workspace', async () => {
170+
mocks.get.mockRejectedValue(new OrchestrationError('not_found', 'Workspace not found'))
171+
172+
const response = await GET(request('GET'), context)
173+
174+
expect(response.status).toBe(404)
175+
expect((await response.json()).error).toEqual({
176+
code: 'NOT_FOUND',
177+
message: 'Sandbox not found',
178+
})
179+
})
180+
169181
it('keeps an in-workspace role refusal a 403 with its remedy', async () => {
170182
mocks.update.mockRejectedValue(new InsufficientWorkspacePermissionsError())
171183

apps/sim/app/api/v2/sandboxes/utils.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,21 @@ export function renderSandboxError(error: unknown) {
3737
return v2CaughtOrchestrationError(error)
3838
}
3939

40+
/**
41+
* The item routes answer every absence as a missing sandbox. A missing or
42+
* archived workspace must not read differently from a workspace the caller has
43+
* no reach into, or the message becomes an oracle for which workspace ids exist.
44+
*/
45+
function renderSandboxResourceError(error: unknown) {
46+
if (asOrchestrationError(error)?.code === 'not_found') {
47+
return v2Error('NOT_FOUND', SANDBOX_NOT_FOUND_MESSAGE)
48+
}
49+
return renderSandboxError(error)
50+
}
51+
4052
export const sandboxCollectionErrorPolicy: V2ErrorPolicy = { render: renderSandboxError }
4153

4254
export const sandboxResourceErrorPolicy = createV2ResourceConcealmentPolicy({
4355
notFoundMessage: SANDBOX_NOT_FOUND_MESSAGE,
44-
render: renderSandboxError,
56+
render: renderSandboxResourceError,
4557
})

apps/sim/app/api/workspaces/[id]/sandboxes/error-policy.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,22 @@ describe('internal sandbox error policy', () => {
118118
internalSandboxResourceErrorPolicy.project(new InsufficientWorkspacePermissionsError())
119119
).toMatchObject({ status: 403 })
120120
})
121+
122+
/**
123+
* A missing workspace and a workspace the caller cannot reach are both a
124+
* missing sandbox on item routes; a distinct message would tell a probe which
125+
* workspace ids exist. The collection policy keeps the specific message.
126+
*/
127+
it('answers a missing workspace on item routes as a missing sandbox', () => {
128+
const missingWorkspace = new OrchestrationError('not_found', 'Workspace not found')
129+
130+
expect(internalSandboxResourceErrorPolicy.project(missingWorkspace)).toMatchObject({
131+
status: 404,
132+
body: { error: 'Sandbox not found' },
133+
})
134+
expect(internalSandboxErrorPolicy.project(missingWorkspace)).toMatchObject({
135+
status: 404,
136+
body: { error: 'Workspace not found' },
137+
})
138+
})
121139
})

apps/sim/app/api/workspaces/[id]/sandboxes/error-policy.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,17 @@ export const internalSandboxErrorPolicy = extendInternalErrorPolicy(
5454
}
5555
)
5656

57-
/** The item routes conceal a caller with no reach into the workspace as a missing sandbox. */
57+
/**
58+
* The item routes answer every absence, and every concealed refusal, as a
59+
* missing sandbox. A missing or archived workspace must not read differently
60+
* from a workspace the caller has no reach into, or the message becomes an
61+
* oracle for which workspace ids exist.
62+
*/
5863
export const internalSandboxResourceErrorPolicy = createInternalResourceConcealmentPolicy({
59-
base: internalSandboxErrorPolicy,
64+
base: extendInternalErrorPolicy(internalSandboxErrorPolicy, (error) =>
65+
asOrchestrationError(error)?.code === 'not_found'
66+
? internalErrorResponse(404, { error: SANDBOX_NOT_FOUND_MESSAGE })
67+
: null
68+
),
6069
notFoundMessage: SANDBOX_NOT_FOUND_MESSAGE,
6170
})

apps/sim/lib/api/contracts/v2/openapi/resources.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,9 +1255,12 @@ const declaredRoutes = [
12551255
resourceOperation('Sandboxes', {
12561256
operationId: 'createSandbox',
12571257
summary: 'Create Sandbox',
1258-
description: `Create a sandbox and schedule its image build. The name must be unique within the workspace. A dependency or system-package entry the builder cannot accept is a \`400\` whose \`error.details\` names the field and the offending entries. ${SANDBOX_PLAN_NOTE} ${WORKSPACE_API_KEY_DENIED}`,
1258+
description: `Create a sandbox. The name must be unique within the workspace. Where the deployment prebuilds dependency images, the build is scheduled and reported through \`buildStatus\`; a deployment that installs at run time, or a sandbox with nothing to install, has no build and reports \`buildStatus: null\`. A dependency or system-package entry the builder cannot accept is a \`400\` whose \`error.details\` names the field and the offending entries. ${SANDBOX_PLAN_NOTE} ${WORKSPACE_API_KEY_DENIED}`,
12591259
errors: RESOURCE_CONFLICT_ERRORS,
1260-
success: { description: 'The sandbox was created and its build scheduled.' },
1260+
success: {
1261+
description:
1262+
'The sandbox was created; a build is scheduled where the deployment prebuilds images.',
1263+
},
12611264
}),
12621265
{
12631266
query: v2CreateSandboxContract.query,
@@ -1280,7 +1283,7 @@ const declaredRoutes = [
12801283
v2CreateSandboxContract.response.schema,
12811284
'CreateSandboxResponse',
12821285
'Create sandbox response',
1283-
'The created sandbox, with its build still pending.',
1286+
'The created sandbox. `buildStatus` is `pending` while an image builds and `null` where nothing is built.',
12841287
[{ data: { ...SANDBOX_EXAMPLE, buildStatus: 'pending', builtAt: null } }]
12851288
),
12861289
}
@@ -1322,7 +1325,7 @@ const declaredRoutes = [
13221325
resourceOperation('Sandboxes', {
13231326
operationId: 'updateSandbox',
13241327
summary: 'Update Sandbox',
1325-
description: `Update the supplied sandbox fields and rebuild the image. Omitted fields retain their stored values; a supplied list replaces the whole list; names must remain unique within the workspace. Re-sending an unchanged spec after a failed build retries the build. ${SANDBOX_PLAN_NOTE} ${WORKSPACE_API_KEY_DENIED}`,
1328+
description: `Update the supplied sandbox fields. Omitted fields retain their stored values; a supplied list replaces the whole list; names must remain unique within the workspace. Where the deployment prebuilds dependency images, a changed spec is rebuilt and re-sending an unchanged spec after a failed build retries it; a deployment that installs at run time, or a spec with nothing to install, has no build and reports \`buildStatus: null\`. ${SANDBOX_PLAN_NOTE} ${WORKSPACE_API_KEY_DENIED}`,
13261329
errors: RESOURCE_CONFLICT_ERRORS,
13271330
success: { description: 'The updated sandbox.' },
13281331
}),
@@ -1345,7 +1348,7 @@ const declaredRoutes = [
13451348
v2UpdateSandboxContract.response.schema,
13461349
'UpdateSandboxResponse',
13471350
'Update sandbox response',
1348-
'The updated sandbox, with its rebuild pending.',
1351+
'The updated sandbox. `buildStatus` is `pending` while an image rebuilds and `null` where nothing is built.',
13491352
[
13501353
{
13511354
data: {
@@ -1364,7 +1367,7 @@ const declaredRoutes = [
13641367
resourceOperation('Sandboxes', {
13651368
operationId: 'deleteSandbox',
13661369
summary: 'Delete Sandbox',
1367-
description: `Delete a sandbox. Function blocks that still select it fail closed at run time until they are re-pointed, and the image is released once nothing else shares it. ${SANDBOX_PLAN_NOTE} ${WORKSPACE_API_KEY_DENIED}`,
1370+
description: `Delete a sandbox. Function blocks that still select it fail closed at run time until they are re-pointed. Any prebuilt image is released once nothing else shares it. ${SANDBOX_PLAN_NOTE} ${WORKSPACE_API_KEY_DENIED}`,
13681371
errors: RESOURCE_ERRORS,
13691372
success: { description: 'The sandbox was deleted.' },
13701373
}),

0 commit comments

Comments
 (0)