Skip to content

Commit 378cb1c

Browse files
committed
fix(v2-api): close a third secret disclosure and make concealment coherent
**Secret disclosure — run snapshot.** `GET /api/v2/logs/{runId}` returned `workflowState` straight from `workflowExecutionSnapshots.stateData`, which is the workflow graph: `blocks[].subBlocks[].value` holds `password: true` field values and `oauth-input` credential ids. Nothing on that path sanitized it, and the field was typed `z.unknown()`, so the builder's response parse stripped nothing. A read-role workspace API key could read plaintext credentials. This is the third instance of one pattern, and the pattern is the finding: the builder protects every response by re-parsing it, so the only fields that can leak are the ones typed `z.unknown()` or `z.custom()`. Both prior disclosures sat behind exactly such a field. The snapshot is now sanitized in the use case and the field is typed object-or-null. An inventory of every remaining `z.unknown()` in the v2 contracts is in the PR description; two carry data with no projection behind them and are named there as follow-ups. **Concealment was bypassable.** `createV2ResourceConcealmentPolicy` rewrites resource-authorization failures to 404 so a caller cannot probe for existence. Workflows and files applied it on every verb; tables and knowledge applied it only on reads. A caller could therefore probe with PATCH, read the 403, and learn the resource exists — the read-side concealment bought nothing. Nine mutation sites now conceal, plus the three table-column verbs, which were inconsistent with their own sibling sub-resources. `lib/logs/api/route-policies.ts` was a second, divergent implementation that sniffed `response.status === 403` and so also swallowed workspace-policy denials the canonical helper deliberately preserves. It now uses the helper. A third such sniff survives in the upload-control helper and is noted as a follow-up. Also: - `DELETE /tables/{tableId}/rows/{rowId}` returned the bulk `{deletedCount, deletedRowIds}` shape while nine sibling single-resource deletes return `{id, deleted}`. It now matches them. - Nine operations can 404 on an unknown folder path and did not document it; `createWorkflow` could 413 on an oversized folder tree and did not; getting a run can 409 when trace data was truncated and did not. - `queryTableRows` documented a 413 it cannot emit and `resumeWorkflowRun` a 423 with no lock guard anywhere in its path — the same un-producible-status class already cleared for 410 elsewhere. - Execute's 409 description covered only the run-id case after the recursion-guard fix added a second cause, and named a code the route does not emit: the wire carries `error.code: CONFLICT` with the specific cause in `error.details.code`. `x-sim-via` is now a declared request header. - Deploy and rollback published examples that were impossible: `isDeployed: true` beside `activeDeployment: null`, where the route computes the former from the latter. - `afterRowId`/`beforeRowId` were published on row insert and silently dropped by the route, so a positional insert became a tail append. - A generated document whose script fails permanently answered "still being generated, try again" forever; the underlying cause is now preserved.
1 parent e5a2428 commit 378cb1c

39 files changed

Lines changed: 401 additions & 138 deletions

File tree

apps/docs/openapi-v2-billing.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@
364364
}
365365
},
366366
"RunIdConflict": {
367-
"description": "The run identifier is already associated with a different request.",
367+
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
368368
"headers": {
369369
"X-Run-Id": {
370370
"$ref": "#/components/headers/X-Run-Id"

apps/docs/openapi-v2-files-audit.json

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@
154154
"403": {
155155
"$ref": "#/components/responses/Forbidden"
156156
},
157+
"404": {
158+
"$ref": "#/components/responses/NotFound"
159+
},
157160
"429": {
158161
"$ref": "#/components/responses/RateLimited"
159162
},
@@ -281,6 +284,9 @@
281284
"403": {
282285
"$ref": "#/components/responses/Forbidden"
283286
},
287+
"404": {
288+
"$ref": "#/components/responses/NotFound"
289+
},
284290
"429": {
285291
"$ref": "#/components/responses/RateLimited"
286292
},
@@ -1981,7 +1987,7 @@
19811987
}
19821988
},
19831989
"RunIdConflict": {
1984-
"description": "The run identifier is already associated with a different request.",
1990+
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
19851991
"headers": {
19861992
"X-Run-Id": {
19871993
"$ref": "#/components/headers/X-Run-Id"
@@ -2134,12 +2140,12 @@
21342140
"size": {
21352141
"type": "number",
21362142
"minimum": 0,
2137-
"description": "File size in bytes.",
2143+
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source rather than the rendered document, so this does not predict how many bytes `GET /files/{fileId}` returns — that endpoint serves the compiled artifact, which is typically much larger.",
21382144
"examples": [1024]
21392145
},
21402146
"type": {
21412147
"type": "string",
2142-
"description": "MIME type of the file.",
2148+
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source, so this describes the source and not what `GET /files/{fileId}` serves — that endpoint returns the compiled artifact under the rendered document type.",
21432149
"examples": ["text/csv"]
21442150
},
21452151
"key": {
@@ -2744,12 +2750,12 @@
27442750
"size": {
27452751
"type": "number",
27462752
"minimum": 0,
2747-
"description": "File size in bytes.",
2753+
"description": "Size in bytes of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source rather than the rendered document, so this does not predict how many bytes `GET /files/{fileId}` returns — that endpoint serves the compiled artifact, which is typically much larger.",
27482754
"examples": [1024]
27492755
},
27502756
"type": {
27512757
"type": "string",
2752-
"description": "MIME type of the file.",
2758+
"description": "MIME type of the stored file. For a generated document (docx, pptx, pdf, xlsx) the stored file is the generation source, so this describes the source and not what `GET /files/{fileId}` serves — that endpoint returns the compiled artifact under the rendered document type.",
27532759
"examples": ["text/csv"]
27542760
},
27552761
"key": {

apps/docs/openapi-v2-knowledge.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1786,7 +1786,7 @@
17861786
}
17871787
},
17881788
"RunIdConflict": {
1789-
"description": "The run identifier is already associated with a different request.",
1789+
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
17901790
"headers": {
17911791
"X-Run-Id": {
17921792
"$ref": "#/components/headers/X-Run-Id"

apps/docs/openapi-v2-logs.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@
288288
"get": {
289289
"operationId": "getLog",
290290
"summary": "Get Log",
291-
"description": "Retrieve the diagnostic representation of a run, including its workflow snapshot, trace spans, final output, and cost. Trace spans are stored separately from the log row and are pruned on their own retention schedule: a run whose stored spans have aged out returns `traceSpans: []` rather than an error, so an empty array does not mean the run recorded no spans.",
291+
"description": "Retrieve the diagnostic representation of a run, including its workflow snapshot, trace spans, final output, and cost. The returned `workflowState` snapshot has credential values redacted: OAuth credential references and secret (`password`) sub-block values are null, while `{{VAR}}` environment-variable references are preserved so consecutive snapshots stay diffable. Trace spans are stored separately from the log row and are pruned on their own retention schedule: a run whose stored spans have aged out returns `traceSpans: []` rather than an error, so an empty array does not mean the run recorded no spans.",
292292
"tags": ["Logs"],
293293
"parameters": [
294294
{
@@ -472,7 +472,7 @@
472472
}
473473
},
474474
"RunIdConflict": {
475-
"description": "The run identifier is already associated with a different request.",
475+
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
476476
"headers": {
477477
"X-Run-Id": {
478478
"$ref": "#/components/headers/X-Run-Id"
@@ -1194,7 +1194,19 @@
11941194
"description": "Workflow snapshot associated with the execution."
11951195
},
11961196
"workflowState": {
1197-
"description": "Workflow state snapshot captured for the run."
1197+
"anyOf": [
1198+
{
1199+
"type": "object",
1200+
"properties": {},
1201+
"additionalProperties": {
1202+
"description": "One top-level snapshot section — `blocks`, `edges`, `loops`, `parallels`, or `variables` — passed through as stored."
1203+
}
1204+
},
1205+
{
1206+
"type": "null"
1207+
}
1208+
],
1209+
"description": "Workflow graph snapshot captured for the run, with credential values redacted: `oauth-input` values and `password: true` sub-block values are null, while `{{VAR}}` environment-variable references are preserved. Null when no snapshot is retained."
11981210
},
11991211
"traceSpans": {
12001212
"type": "array",

apps/docs/openapi-v2-resources.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1923,7 +1923,7 @@
19231923
}
19241924
},
19251925
"RunIdConflict": {
1926-
"description": "The run identifier is already associated with a different request.",
1926+
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
19271927
"headers": {
19281928
"X-Run-Id": {
19291929
"$ref": "#/components/headers/X-Run-Id"

apps/docs/openapi-v2-tables.json

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@
152152
"403": {
153153
"$ref": "#/components/responses/Forbidden"
154154
},
155+
"404": {
156+
"$ref": "#/components/responses/NotFound"
157+
},
155158
"413": {
156159
"$ref": "#/components/responses/PayloadTooLarge"
157160
},
@@ -213,6 +216,9 @@
213216
"403": {
214217
"$ref": "#/components/responses/Forbidden"
215218
},
219+
"404": {
220+
"$ref": "#/components/responses/NotFound"
221+
},
216222
"409": {
217223
"$ref": "#/components/responses/Conflict"
218224
},
@@ -1437,9 +1443,6 @@
14371443
"404": {
14381444
"$ref": "#/components/responses/NotFound"
14391445
},
1440-
"413": {
1441-
"$ref": "#/components/responses/PayloadTooLarge"
1442-
},
14431446
"429": {
14441447
"$ref": "#/components/responses/RateLimited"
14451448
},
@@ -2467,6 +2470,9 @@
24672470
"403": {
24682471
"$ref": "#/components/responses/Forbidden"
24692472
},
2473+
"404": {
2474+
"$ref": "#/components/responses/NotFound"
2475+
},
24702476
"409": {
24712477
"$ref": "#/components/responses/Conflict"
24722478
},
@@ -3329,6 +3335,9 @@
33293335
"403": {
33303336
"$ref": "#/components/responses/Forbidden"
33313337
},
3338+
"404": {
3339+
"$ref": "#/components/responses/NotFound"
3340+
},
33323341
"413": {
33333342
"$ref": "#/components/responses/PayloadTooLarge"
33343343
},
@@ -3694,7 +3703,7 @@
36943703
}
36953704
},
36963705
"RunIdConflict": {
3697-
"description": "The run identifier is already associated with a different request.",
3706+
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
36983707
"headers": {
36993708
"X-Run-Id": {
37003709
"$ref": "#/components/headers/X-Run-Id"
@@ -4981,22 +4990,20 @@
49814990
"V2DeleteRowData": {
49824991
"type": "object",
49834992
"properties": {
4984-
"deletedCount": {
4985-
"type": "number",
4986-
"description": "Number of deleted rows."
4987-
},
4988-
"deletedRowIds": {
4989-
"type": "array",
4990-
"items": {
4991-
"type": "string"
4992-
},
4993+
"id": {
4994+
"type": "string",
49934995
"description": "Identifier of the deleted row."
4996+
},
4997+
"deleted": {
4998+
"type": "boolean",
4999+
"const": true,
5000+
"description": "Confirms that the row was deleted."
49945001
}
49955002
},
4996-
"required": ["deletedCount", "deletedRowIds"],
5003+
"required": ["id", "deleted"],
49975004
"additionalProperties": false,
49985005
"title": "Delete row data",
4999-
"description": "Result of a single-row deletion."
5006+
"description": "Row deletion acknowledgement."
50005007
},
50015008
"V2DeleteTableRowResponse": {
50025009
"type": "object",
@@ -5009,7 +5016,7 @@
50095016
"required": ["data"],
50105017
"additionalProperties": false,
50115018
"title": "Delete table row response",
5012-
"description": "Deleted row count and identifier."
5019+
"description": "Row deletion acknowledgement."
50135020
},
50145021
"V2UpsertRowData": {
50155022
"type": "object",

apps/docs/openapi-v2-workflows.json

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,9 @@
165165
"403": {
166166
"$ref": "#/components/responses/Forbidden"
167167
},
168+
"404": {
169+
"$ref": "#/components/responses/NotFound"
170+
},
168171
"413": {
169172
"$ref": "#/components/responses/PayloadTooLarge"
170173
},
@@ -182,7 +185,7 @@
182185
"post": {
183186
"operationId": "createWorkflowV2",
184187
"summary": "Create Workflow",
185-
"description": "Create a workflow in a workspace root or canonical workflow folder.",
188+
"description": "Create a workflow in a workspace root or canonical workflow folder. A workspace whose folder tree exceeds 10,000 folders is a 413, because the response needs the whole tree to render folder paths.",
186189
"tags": ["Workflows"],
187190
"requestBody": {
188191
"required": true,
@@ -226,9 +229,15 @@
226229
"403": {
227230
"$ref": "#/components/responses/Forbidden"
228231
},
232+
"404": {
233+
"$ref": "#/components/responses/NotFound"
234+
},
229235
"409": {
230236
"$ref": "#/components/responses/Conflict"
231237
},
238+
"413": {
239+
"$ref": "#/components/responses/PayloadTooLarge"
240+
},
232241
"423": {
233242
"$ref": "#/components/responses/Locked"
234243
},
@@ -1046,6 +1055,16 @@
10461055
"pattern": "^[A-Za-z0-9._:-]+$",
10471056
"examples": ["run_8f14e45f-ceea-467f-a"]
10481057
}
1058+
},
1059+
{
1060+
"name": "x-sim-via",
1061+
"in": "header",
1062+
"required": false,
1063+
"description": "Comma-separated workflow identifiers describing the workflow-to-workflow call chain that led to this request. Each hop appends its own workflow id, and Sim sets it automatically when one workflow calls another; supply it yourself only when relaying an existing chain. A chain already at the maximum depth is rejected with 409 and `error.details.code: \"CALL_CHAIN_DEPTH_EXCEEDED\"`, which is how runaway recursion between workflows is stopped.",
1064+
"schema": {
1065+
"description": "Comma-separated workflow identifiers describing the workflow-to-workflow call chain that led to this request. Each hop appends its own workflow id, and Sim sets it automatically when one workflow calls another; supply it yourself only when relaying an existing chain. A chain already at the maximum depth is rejected with 409 and `error.details.code: \"CALL_CHAIN_DEPTH_EXCEEDED\"`, which is how runaway recursion between workflows is stopped.",
1066+
"type": "string"
1067+
}
10491068
}
10501069
],
10511070
"requestBody": {
@@ -1385,6 +1404,9 @@
13851404
"404": {
13861405
"$ref": "#/components/responses/NotFound"
13871406
},
1407+
"409": {
1408+
"$ref": "#/components/responses/Conflict"
1409+
},
13881410
"429": {
13891411
"$ref": "#/components/responses/RateLimited"
13901412
},
@@ -1511,9 +1533,6 @@
15111533
"413": {
15121534
"$ref": "#/components/responses/PayloadTooLarge"
15131535
},
1514-
"423": {
1515-
"$ref": "#/components/responses/Locked"
1516-
},
15171536
"429": {
15181537
"$ref": "#/components/responses/RateLimited"
15191538
},
@@ -1704,6 +1723,9 @@
17041723
"403": {
17051724
"$ref": "#/components/responses/Forbidden"
17061725
},
1726+
"404": {
1727+
"$ref": "#/components/responses/NotFound"
1728+
},
17071729
"413": {
17081730
"$ref": "#/components/responses/PayloadTooLarge"
17091731
},
@@ -2075,7 +2097,7 @@
20752097
}
20762098
},
20772099
"RunIdConflict": {
2078-
"description": "The run identifier is already associated with a different request.",
2100+
"description": "The run cannot be started. Two causes share this status, distinguished by `error.details.code`: `RUN_ID_CONFLICT` when the supplied `X-Run-Id` is already associated with a different request, and `CALL_CHAIN_DEPTH_EXCEEDED` when the incoming `X-Sim-Via` chain has already reached the maximum workflow-to-workflow call depth.",
20792101
"headers": {
20802102
"X-Run-Id": {
20812103
"$ref": "#/components/headers/X-Run-Id"
@@ -3128,11 +3150,26 @@
31283150
{
31293151
"data": {
31303152
"id": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
3131-
"isDeployed": true,
3132-
"deployedAt": "2026-06-12T10:30:00.000Z",
3153+
"isDeployed": false,
3154+
"deployedAt": null,
31333155
"warnings": [],
31343156
"activeDeployment": null,
3135-
"latestDeploymentAttempt": null,
3157+
"latestDeploymentAttempt": {
3158+
"id": "depop_01J8ZK3QW4M6X2R9T7B5C0V1",
3159+
"deploymentVersionId": "depver_01J8ZK3QW4M6X2R9T7B5C0V2",
3160+
"version": 3,
3161+
"action": "deploy",
3162+
"status": "preparing",
3163+
"isCurrent": true,
3164+
"readiness": {
3165+
"webhooks": "pending",
3166+
"schedules": "ready",
3167+
"mcp": "not_applicable"
3168+
},
3169+
"requestedAt": "2026-06-12T10:30:00.000Z",
3170+
"activatedAt": null,
3171+
"error": null
3172+
},
31363173
"version": 3
31373174
}
31383175
}
@@ -3352,11 +3389,26 @@
33523389
{
33533390
"data": {
33543391
"id": "3b1f7c92-8d4e-4a6b-9c0d-5e2f8a714b36",
3355-
"isDeployed": true,
3356-
"deployedAt": "2026-06-12T10:30:00.000Z",
3392+
"isDeployed": false,
3393+
"deployedAt": null,
33573394
"warnings": [],
33583395
"activeDeployment": null,
3359-
"latestDeploymentAttempt": null,
3396+
"latestDeploymentAttempt": {
3397+
"id": "depop_01J8ZK4RX5N7Y3S0U8D6E1W2",
3398+
"deploymentVersionId": "depver_01J8ZK4RX5N7Y3S0U8D6E1W3",
3399+
"version": 2,
3400+
"action": "activate",
3401+
"status": "activating",
3402+
"isCurrent": true,
3403+
"readiness": {
3404+
"webhooks": "ready",
3405+
"schedules": "ready",
3406+
"mcp": "not_applicable"
3407+
},
3408+
"requestedAt": "2026-06-12T10:30:00.000Z",
3409+
"activatedAt": null,
3410+
"error": null
3411+
},
33603412
"version": 2
33613413
}
33623414
}

0 commit comments

Comments
 (0)