@com55/node-red-ai-access 0.2.0-beta.6 → 0.2.0-beta.8

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.
@@ -2,7 +2,7 @@ openapi: 3.1.0
2
2
 
3
3
  info:
4
4
  title: Node-RED AI Access Plugin API
5
- version: 0.1.1
5
+ version: 0.2.0-beta.8
6
6
  summary: Temporary, owner-approved Node-RED access for AI clients
7
7
  description: |
8
8
  REST surface exposed by the `@com55/node-red-ai-access` Node-RED plugin
@@ -10,8 +10,8 @@ info:
10
10
 
11
11
  **Git mental model:** a proposal is like a pull request. Human approve =
12
12
  merge; reject = close; withdraw = the author closes their own PR. If `rev` /
13
- `expectedRev` does not match, treat it as a conflict: re-read the flow,
14
- rebase the edit, and resubmit. Every access link can see all proposals
13
+ `expectedRev` does not match, open proposals auto-rebase; only overlapping
14
+ field changes block approval. Read `rebase.state` to assess readiness. Every access link can see all proposals
15
15
  (`mine: true|false`).
16
16
 
17
17
  ## Base path
@@ -173,7 +173,7 @@ info:
173
173
  (`invalid_filter` for list filters, `invalid_arguments` otherwise).
174
174
 
175
175
  On submit and on **get** of a **pending** proposal, responses may include
176
- `conflictingProposals` (same-`expectedRev` peers or true node/field overlaps)
176
+ `conflictingProposals` (open proposals with overlapping node edits)
177
177
  with optional `overlappingNodeIds` / `overlappingFields`. List endpoints
178
178
  omit `conflictingProposals`. Submit, get, and list responses include
179
179
  `autoClosedCount` when that request auto-closed pending proposals as
@@ -184,17 +184,25 @@ info:
184
184
  `N validation errors` and every entry in `errors[]`. Duplicate node ids in a
185
185
  full tab or batch return `duplicate_node_id`.
186
186
 
187
- Pending proposals whose `expectedRev` no longer matches the live flow rev
188
- are auto-closed as `expired`. When a proposal is approved, other pending
189
- proposals on the same flow become `superseded`. Closed proposals are never
190
- conflicts.
187
+ Open proposals (`pending` or `conflict`) auto-rebase when live revisions change.
188
+ Disjoint node and tab fields merge automatically. Only fields changed on both
189
+ sides conflict (even identical edits); x/y never conflict and proposed positions
190
+ win ties. Env merges by unique name and entry field, credentials by key.
191
+ Duplicate env names fall back to atomic comparison with ambiguity warnings.
192
+ Conflicts block approval until resolved. Proposals expire only after the
193
+ configured `proposalTtlDays` (default 7). Approval rebases other open proposals;
194
+ legacy expired, rejected and superseded proposals stay closed.
191
195
 
192
196
  ## Withdraw
193
197
 
194
198
  The creating access link may withdraw its own pending proposal
195
199
  (`POST /proposals/{id}/withdraw`, GET `…/proposals/{id}/withdraw?confirm=1`,
196
200
  or MCP `withdraw_proposal`). GET withdraw requires `confirm=1` so prefetchers
197
- cannot trigger it. Status becomes `withdrawn` (separate from admin
201
+ cannot trigger it. If the token is lost, a full (`*`) admin session may use
202
+ POST `/proposals/{id}/withdraw`. Other tokens remain blocked. Recovery is logged
203
+ as `proposal.withdrawn` with the admin identity, originalOwnerId, reason and
204
+ withdrawalAuthority=`admin-recovery` (owner-token withdrawals use `owner-token`).
205
+ Status becomes `withdrawn` (separate from admin
198
206
  `rejected`).
199
207
 
200
208
  ## Known issues
@@ -963,10 +971,9 @@ paths:
963
971
  flow tab. Does not mutate Node-RED until an admin with `*` approves.
964
972
 
965
973
  Include `expectedRev` (preferred; `expected_rev` accepted as alias)
966
- from the latest `GET /flows` or `GET /flows/{id}` response. The live
967
- flow revision is checked **at submit time** (**409**
968
- `version_mismatch` if stale). On approve, a stale revision marks the
969
- proposal `rejected` with a resubmit message (no deploy).
974
+ from `GET /flows` or `GET /flows/{id}`. Recent known stale revisions are
975
+ accepted using snapshot history and rebased immediately. Unknown revisions
976
+ return **409** `version_mismatch`. Approval rechecks against the live revision.
970
977
 
971
978
  Optional `title` (≤80 chars) is stored as a human-friendly label; when
972
979
  omitted it is auto-generated from the change set. Responses always
@@ -1055,14 +1062,18 @@ paths:
1055
1062
  (long text fields listed for expandable UI). Modified nodes include
1056
1063
  line-level diffs for text fields such as `func` and `template`.
1057
1064
 
1058
- `baseRevStale` is true only for **pending** proposals when
1059
- `expectedRev` does not match the live revision (approve would fail).
1060
- Approved/rejected/failed proposals never set `baseRevStale`. When
1061
- pending and stale, `vsCurrent` optionally includes proposal-vs-live
1062
- sections for review.
1063
-
1064
- Admins with `read` can diff any proposal. Access-link clients can only
1065
- diff proposals they created.
1065
+ `baseRev` aliases `expectedRev`, the stored proposal base revision.
1066
+ `baseRevChanged` is true only for **open** proposals when
1067
+ `expectedRev` does not match the live revision (informational).
1068
+ This is informational after a successful rebase, not an approval error.
1069
+ `rebase.state` (`clean`, `merged`, `conflict`) reports merge readiness and
1070
+ `rebase.rev` identifies the checked live rev. Closed proposals never set
1071
+ `baseRevChanged` as true. Open proposals include
1072
+ `rebase` and `liveChanges` (editor changes since the stored base).
1073
+ Without conflicts, `vsCurrent` compares the merged deployment to live.
1074
+ Merges use raw stored proposals and live flows; AI diff sections are redacted.
1075
+
1076
+ Admins with `read` and access-link clients can diff any proposal.
1066
1077
  operationId: getProposalDiff
1067
1078
  security:
1068
1079
  - AccessLinkBearer: []
@@ -1085,6 +1096,102 @@ paths:
1085
1096
  '404':
1086
1097
  $ref: '#/components/responses/NotFound'
1087
1098
 
1099
+ /proposals/{id}/resolve-conflict:
1100
+ post:
1101
+ tags: [Proposals]
1102
+ summary: Choose live or proposed for one current conflict, or undo the latest choice
1103
+ operationId: resolveProposalConflict
1104
+ security:
1105
+ - AdminSession: []
1106
+ parameters:
1107
+ - $ref: '#/components/parameters/ProposalId'
1108
+ requestBody:
1109
+ required: true
1110
+ content:
1111
+ application/json:
1112
+ schema:
1113
+ $ref: '#/components/schemas/ConflictChoiceRequest'
1114
+ responses:
1115
+ '200':
1116
+ description: Presented proposal with current rebase metadata
1117
+ content:
1118
+ application/json:
1119
+ schema:
1120
+ $ref: '#/components/schemas/Proposal'
1121
+ '400':
1122
+ $ref: '#/components/responses/BadRequest'
1123
+ '401':
1124
+ $ref: '#/components/responses/Unauthorized'
1125
+ '403':
1126
+ $ref: '#/components/responses/Forbidden'
1127
+ '404':
1128
+ description: Proposal not found
1129
+ '409':
1130
+ description: Proposal closed, conflict missing, or revision unavailable
1131
+ /proposals/{id}/recheck:
1132
+ post:
1133
+ tags: [Proposals]
1134
+ summary: Recheck an open proposal against raw live flows
1135
+ operationId: recheckProposal
1136
+ security:
1137
+ - AdminSession: []
1138
+ parameters:
1139
+ - $ref: '#/components/parameters/ProposalId'
1140
+ responses:
1141
+ '200':
1142
+ description: Presented proposal with current rebase metadata
1143
+ content:
1144
+ application/json:
1145
+ schema:
1146
+ $ref: '#/components/schemas/Proposal'
1147
+ '400':
1148
+ $ref: '#/components/responses/BadRequest'
1149
+ '401':
1150
+ $ref: '#/components/responses/Unauthorized'
1151
+ '403':
1152
+ $ref: '#/components/responses/Forbidden'
1153
+ '404':
1154
+ description: Proposal not found
1155
+ '409':
1156
+ description: Proposal closed, conflict missing, or revision unavailable
1157
+ /proposals/{id}/resolve:
1158
+ post:
1159
+ tags: [Proposals]
1160
+ summary: Resolve your own open proposal with a choice or replacement payload
1161
+ description: |
1162
+ Use nodeId + choice for one current conflict (overlapping fields only;
1163
+ independent live and proposed edits survive). Or use expectedRev + payload:
1164
+ operations apply to the flow snapshot at expectedRev, not the stored
1165
+ proposal or merged result. Include every intended edit. Do not mix modes.
1166
+ operationId: resolveProposal
1167
+ security:
1168
+ - AccessLinkBearer: []
1169
+ parameters:
1170
+ - $ref: '#/components/parameters/ProposalId'
1171
+ requestBody:
1172
+ required: true
1173
+ content:
1174
+ application/json:
1175
+ schema:
1176
+ $ref: '#/components/schemas/ResolveProposalRequest'
1177
+ responses:
1178
+ '200':
1179
+ description: Presented proposal with current rebase metadata
1180
+ content:
1181
+ application/json:
1182
+ schema:
1183
+ $ref: '#/components/schemas/Proposal'
1184
+ '400':
1185
+ $ref: '#/components/responses/BadRequest'
1186
+ '401':
1187
+ $ref: '#/components/responses/Unauthorized'
1188
+ '403':
1189
+ $ref: '#/components/responses/Forbidden'
1190
+ '404':
1191
+ description: Proposal not found
1192
+ '409':
1193
+ description: Proposal closed, conflict missing, or revision unavailable
1194
+
1088
1195
  /proposals/{id}/approve:
1089
1196
  post:
1090
1197
  tags: [Proposals]
@@ -1094,9 +1201,12 @@ paths:
1094
1201
  Optional body `deploymentType`: `nodes` (default from settings),
1095
1202
  `flows`, or `full`.
1096
1203
 
1097
- Revision conflict: proposal is marked `rejected` with a resubmit
1098
- message (HTTP 200 with `status: rejected`). Other deploy errors mark
1099
- it `failed`. Concurrent approve of the same id returns 409.
1204
+ Auto-rebases against raw live flows and deploys the merged tab with the live rev.
1205
+ Conflicts return 409 `proposal_conflict` with field-name-only `conflicts`.
1206
+ Merged editor changes require `mergedHash` from the reviewed diff; otherwise
1207
+ 409 `rebase_ack_required` includes the current hash. Revision races retry
1208
+ up to three times then return 409 `rebase_race`, leaving the proposal open.
1209
+ Other deploy errors mark it `failed`. Concurrent approval returns 409.
1100
1210
  operationId: approveProposal
1101
1211
  security:
1102
1212
  - AdminSession: []
@@ -1179,14 +1289,16 @@ paths:
1179
1289
  /proposals/{id}/withdraw:
1180
1290
  post:
1181
1291
  tags: [Proposals]
1182
- summary: Withdraw own pending proposal
1292
+ summary: Withdraw an open proposal (owner token or full admin recovery)
1183
1293
  description: |
1184
- Access-link Bearer only. Withdraws a **pending** proposal created by
1185
- this token. Status becomes `withdrawn` (not `rejected`). Optional
1294
+ Accepts the creating access-link Bearer, or a full (`*`) admin session
1295
+ for lost-token recovery. Other tokens and read-only admins are forbidden.
1296
+ Withdraws an **open** (pending or conflict) proposal. Status becomes `withdrawn` (not `rejected`). Optional
1186
1297
  JSON body `{ "reason": "…" }`.
1187
1298
  operationId: withdrawProposal
1188
1299
  security:
1189
1300
  - AccessLinkBearer: []
1301
+ - AdminSession: []
1190
1302
  parameters:
1191
1303
  - $ref: '#/components/parameters/ProposalId'
1192
1304
  requestBody:
@@ -1208,7 +1320,7 @@ paths:
1208
1320
  '401':
1209
1321
  $ref: '#/components/responses/Unauthorized'
1210
1322
  '403':
1211
- description: Not owned by this access link
1323
+ description: Not owned by this access link or admin lacks full permission
1212
1324
  content:
1213
1325
  application/json:
1214
1326
  schema:
@@ -1369,8 +1481,8 @@ components:
1369
1481
  code: access_link_invalid
1370
1482
  VersionConflict:
1371
1483
  description: |
1372
- `expectedRev` does not match the current flow revision
1373
- (`code: version_mismatch`). Re-read flows and resubmit.
1484
+ `expectedRev` is unavailable in recent snapshot history and does not
1485
+ match the live revision (`code: version_mismatch`). Re-read flows and resubmit.
1374
1486
  content:
1375
1487
  application/json:
1376
1488
  schema:
@@ -1456,6 +1568,14 @@ components:
1456
1568
  description: |
1457
1569
  Machine-readable `code` on JSON error bodies. See docs/errors.md.
1458
1570
 
1571
+ - `approve_queue_timeout` (504): Approval exceeded its queue-wait deadline (default 60 seconds); no deployment was started for this request
1572
+ - `deploy_timeout` (504): Deployment exceeded its deadline (default 60 seconds); approvals wait for settlement or the hard cap (default 5 minutes)
1573
+ - `proposal_conflict` (409): Editor and proposal changes conflict (conflicts contains field names only)
1574
+ - `rebase_ack_required` (409): The merged result needs review (mergedHash included)
1575
+ - `rebase_race` (409): Flows kept changing during approval
1576
+ - `conflict_not_found` (409): The requested conflict no longer exists
1577
+ - `invalid_choice` (400): Invalid manual conflict choice
1578
+ - `flow_id_mismatch` (400): Resolution tried to change the proposal flow id
1459
1579
  - `access_link_missing` (401): No Bearer / path token
1460
1580
  - `bearer_scheme_required` (401): Auth used Basic (or non-Bearer)
1461
1581
  - `token_expired` (401): Known token past `expiresAt` (body includes `expiredAt` and `accessLinksUrl`)
@@ -1481,10 +1601,10 @@ components:
1481
1601
  - `invalid_diff_format` (400): Unknown `format` on proposal diff
1482
1602
  - `confirm_required` (400): GET withdraw without `confirm=1`
1483
1603
  - `missing_expected_rev` (400): Proposal submitted without `expectedRev` / `rev`
1484
- - `version_mismatch` (409): Stale `rev` / `expectedRev`
1604
+ - `version_mismatch` (409): Unknown or unavailable `rev` / `expectedRev` snapshot
1485
1605
  - `proposal_not_found` (404): Unknown proposal id
1486
- - `proposal_not_pending` (409): Action needs a pending proposal
1487
- - `proposal_not_owned` (403): Withdraw of another token’s proposal
1606
+ - `proposal_not_pending` (409): Action needs an open proposal
1607
+ - `proposal_not_owned` (403): Resolve or withdraw another token’s proposal
1488
1608
  - `validation_failed` (400): Several validation errors at once (message `N validation errors`)
1489
1609
  - `duplicate_node_id` (400): The same node id appears more than once in a full tab or batch
1490
1610
  - `missing_node_id` (400): Node / op without an id
@@ -1509,6 +1629,14 @@ components:
1509
1629
  - `upstream_error` (502): Node-RED runtime/flows error
1510
1630
  - `internal_error` (500): Unexpected server error
1511
1631
  enum:
1632
+ - approve_queue_timeout
1633
+ - deploy_timeout
1634
+ - proposal_conflict
1635
+ - rebase_ack_required
1636
+ - rebase_race
1637
+ - conflict_not_found
1638
+ - invalid_choice
1639
+ - flow_id_mismatch
1512
1640
  - access_link_missing
1513
1641
  - bearer_scheme_required
1514
1642
  - token_expired
@@ -1737,6 +1865,11 @@ components:
1737
1865
  AiAccessSettingsPatch:
1738
1866
  type: object
1739
1867
  properties:
1868
+ proposalTtlDays:
1869
+ type: integer
1870
+ minimum: 1
1871
+ maximum: 365
1872
+ default: 7
1740
1873
  enabled:
1741
1874
  type: boolean
1742
1875
  allowWithoutAdminAuth:
@@ -2184,9 +2317,46 @@ components:
2184
2317
  additionalProperties: true
2185
2318
  additionalProperties: true
2186
2319
 
2320
+ ProposalRebase:
2321
+ type: object
2322
+ description: Merge metadata; conflicts contain field names only, never field values.
2323
+ properties:
2324
+ state:
2325
+ type: string
2326
+ enum: [clean, merged, conflict]
2327
+ rev:
2328
+ type: string
2329
+ mergedHash:
2330
+ type: string
2331
+ conflicts:
2332
+ type: array
2333
+ items:
2334
+ type: object
2335
+ properties:
2336
+ nodeId:
2337
+ type: string
2338
+ kind:
2339
+ type: string
2340
+ fields:
2341
+ type: array
2342
+ items:
2343
+ type: string
2344
+ message:
2345
+ type: string
2346
+ choices:
2347
+ type: array
2348
+ items:
2349
+ type: string
2350
+ enum: [live, proposed]
2351
+ liveChangedCount:
2352
+ type: integer
2353
+ checkedAt:
2354
+ type: string
2355
+ format: date-time
2356
+
2187
2357
  ProposalStatus:
2188
2358
  type: string
2189
- enum: [pending, approved, rejected, failed, withdrawn, expired, superseded]
2359
+ enum: [pending, conflict, approved, rejected, failed, withdrawn, expired, superseded]
2190
2360
 
2191
2361
  Proposal:
2192
2362
  type: object
@@ -2195,6 +2365,7 @@ components:
2195
2365
  - type
2196
2366
  - status
2197
2367
  - expectedRev
2368
+ - baseRev
2198
2369
  - payload
2199
2370
  - createdBy
2200
2371
  - createdAt
@@ -2204,6 +2375,30 @@ components:
2204
2375
  - reviewUrl
2205
2376
  - reviewPath
2206
2377
  properties:
2378
+ conflictChoice:
2379
+ type: object
2380
+ description: Latest manual resolution; admins can undo it until the next body edit or choice. Undo snapshots are never returned.
2381
+ required: [nodeId, choice]
2382
+ properties:
2383
+ nodeId:
2384
+ type: string
2385
+ choice:
2386
+ type: string
2387
+ enum: [live, proposed]
2388
+ deployPending:
2389
+ type: object
2390
+ description: Timed-out deployment has an unknown outcome; approvals wait until settlement or the hard cap. Check live flows before approving again.
2391
+ required: [mergedHash, reason]
2392
+ properties:
2393
+ mergedHash:
2394
+ type: string
2395
+ reason:
2396
+ type: string
2397
+ queueReleased:
2398
+ type: boolean
2399
+ description: True when the deploy hard cap released the queue. Late settlement no longer updates this proposal; approval always re-reads live flows.
2400
+ rebase:
2401
+ $ref: '#/components/schemas/ProposalRebase'
2207
2402
  id:
2208
2403
  type: string
2209
2404
  type:
@@ -2213,6 +2408,12 @@ components:
2213
2408
  $ref: '#/components/schemas/ProposalStatus'
2214
2409
  expectedRev:
2215
2410
  type: string
2411
+ baseRev:
2412
+ type: string
2413
+ description: Revision of the stored base snapshot; alias of expectedRev.
2414
+ baseRevChanged:
2415
+ type: boolean
2416
+ description: Informational revision difference for open proposals; use rebase.state for merge readiness.
2216
2417
  payload:
2217
2418
  $ref: '#/components/schemas/ReplaceFlowPayload'
2218
2419
  baseFlow:
@@ -2318,9 +2519,8 @@ components:
2318
2519
  reason:
2319
2520
  type: string
2320
2521
  description: |
2321
- Present when rejected, failed, or withdrawn. On revision conflict
2322
- after approve, explains that expectedRev is stale and the AI should
2323
- re-read flows and submit a new proposal.
2522
+ Present when rejected, failed, withdrawn, or expired. Revision changes
2523
+ keep open proposals pending or in conflict; they do not close them.
2324
2524
  resultRev:
2325
2525
  type: string
2326
2526
 
@@ -2372,7 +2572,7 @@ components:
2372
2572
  type: object
2373
2573
  required: [kind]
2374
2574
  description: |
2375
- Admin for approve/reject/fail; access-link for withdraw.
2575
+ Admin for approve/reject/fail and recovery withdrawal; access-link for owner withdrawal.
2376
2576
  properties:
2377
2577
  kind:
2378
2578
  type: string
@@ -2424,9 +2624,59 @@ components:
2424
2624
  find: Hello
2425
2625
  replace: Hi
2426
2626
 
2627
+ ConflictChoiceRequest:
2628
+ type: object
2629
+ required: [nodeId, choice]
2630
+ properties:
2631
+ nodeId:
2632
+ type: string
2633
+ choice:
2634
+ type: string
2635
+ enum: [live, proposed, undo]
2636
+ description: Undo restores the proposal before the latest choice for nodeId and rechecks against live. Available until the next body edit or choice; admin only. Nothing is deployed.
2637
+
2638
+ ResolveProposalRequest:
2639
+ oneOf:
2640
+ - type: object
2641
+ additionalProperties: false
2642
+ required: [nodeId, choice]
2643
+ properties:
2644
+ nodeId:
2645
+ type: string
2646
+ description: Current conflict node id; flow id for tab/env conflicts.
2647
+ choice:
2648
+ type: string
2649
+ enum: [live, proposed]
2650
+ description: Keep this side for overlapping fields only; preserve independent changes and live secrets.
2651
+ - type: object
2652
+ required: [expectedRev]
2653
+ properties:
2654
+ expectedRev:
2655
+ type: string
2656
+ description: Flow snapshot revision to which replacement operations apply.
2657
+ title:
2658
+ type: string
2659
+ maxLength: 80
2660
+ payload:
2661
+ $ref: '#/components/schemas/ReplaceFlowPayload'
2662
+ description: Complete intended edits based on expectedRev; not applied to the stored proposal or merged result.
2663
+ nodes:
2664
+ type: array
2665
+ description: Legacy alias for replacement payload nodes, applied to expectedRev.
2666
+ items:
2667
+ type: object
2668
+ additionalProperties: true
2669
+ not:
2670
+ anyOf:
2671
+ - required: [nodeId]
2672
+ - required: [choice]
2673
+
2427
2674
  ApproveProposalRequest:
2428
2675
  type: object
2429
2676
  properties:
2677
+ mergedHash:
2678
+ type: string
2679
+ description: Hash of the reviewed merged result; required when editor changes are merged.
2430
2680
  deploymentType:
2431
2681
  $ref: '#/components/schemas/DeploymentType'
2432
2682
 
@@ -2536,6 +2786,11 @@ components:
2536
2786
  type: object
2537
2787
  required: [summary, added, removed, modified]
2538
2788
  properties:
2789
+ tabProperties:
2790
+ type: array
2791
+ description: Before and after values of changed flow settings, including env. Uses the caller's redacted view.
2792
+ items:
2793
+ $ref: '#/components/schemas/ProposalDiffPropertyChange'
2539
2794
  summary:
2540
2795
  type: object
2541
2796
  required: [added, removed, modified, tabChanges]
@@ -2572,7 +2827,8 @@ components:
2572
2827
  - flowId
2573
2828
  - expectedRev
2574
2829
  - currentRev
2575
- - baseRevStale
2830
+ - baseRevChanged
2831
+ - baseRev
2576
2832
  - compareAgainst
2577
2833
  properties:
2578
2834
  proposalId:
@@ -2581,12 +2837,15 @@ components:
2581
2837
  type: string
2582
2838
  expectedRev:
2583
2839
  type: string
2840
+ baseRev:
2841
+ type: string
2842
+ description: Alias of expectedRev, the stored base revision.
2584
2843
  currentRev:
2585
2844
  type: string
2586
- baseRevStale:
2845
+ baseRevChanged:
2587
2846
  type: boolean
2588
2847
  description: |
2589
- True only for pending proposals when expectedRev ≠ live rev.
2848
+ Informational: true for open proposals when expectedRev ≠ live rev.
2590
2849
  Never true for approved/rejected/failed.
2591
2850
  compareAgainst:
2592
2851
  type: string
@@ -2594,10 +2853,34 @@ components:
2594
2853
  baseSnapshotMissing:
2595
2854
  type: boolean
2596
2855
  description: True for legacy proposals without a stored baseFlow
2856
+ rebase:
2857
+ $ref: '#/components/schemas/ProposalRebase'
2858
+ conflictValues:
2859
+ type: array
2860
+ description: Base, live and proposed values for each conflicting path. AI callers receive redacted values.
2861
+ items:
2862
+ type: object
2863
+ required: [nodeId, fields]
2864
+ properties:
2865
+ nodeId:
2866
+ type: string
2867
+ fields:
2868
+ type: array
2869
+ items:
2870
+ type: object
2871
+ required: [field]
2872
+ properties:
2873
+ field:
2874
+ type: string
2875
+ base: {}
2876
+ live: {}
2877
+ proposed: {}
2878
+ liveChanges:
2879
+ $ref: '#/components/schemas/ProposalDiffSections'
2597
2880
  vsCurrent:
2598
2881
  $ref: '#/components/schemas/ProposalDiffSections'
2599
2882
  description: |
2600
- Present when pending and baseRevStale — proposal vs live flow.
2883
+ Present for open proposals without conflicts — merged result vs live flow.
2601
2884
 
2602
2885
  AuditActor:
2603
2886
  oneOf:
package/docs/errors.md CHANGED
@@ -13,6 +13,14 @@ GET-link `401` responses include a `WWW-Authenticate: Bearer …` header.
13
13
 
14
14
  | Code | HTTP | Meaning | Hint / next step | Channels |
15
15
  | --- | --- | --- | --- | --- |
16
+ | `approve_queue_timeout` | 504 | Approval exceeded its queue-wait deadline (default 60 seconds); no deployment was started for this request | Check the live flow and retry approval | admin |
17
+ | `deploy_timeout` | 504 | Deployment exceeded its deadline (default 60 seconds); approvals wait for settlement or the hard cap (default 5 minutes) | Before the cap, late settlement updates the proposal; after the cap, deployPending.queueReleased is true and the outcome stays unknown. Check the live flow before approving again; approval always re-reads live | admin |
18
+ | `proposal_conflict` | 409 | Editor and proposal changes conflict (conflicts contains field names only) | Read the current flow and resolve the proposal before approval | GET, REST, MCP, admin |
19
+ | `rebase_ack_required` | 409 | The merged result needs review (mergedHash included) | Review the diff and approve with its mergedHash | admin |
20
+ | `rebase_race` | 409 | Flows kept changing during approval | Recheck the proposal and retry approval | admin |
21
+ | `conflict_not_found` | 409 | The requested conflict no longer exists | Reload the proposal and its conflicts | REST, MCP, admin |
22
+ | `invalid_choice` | 400 | Invalid manual conflict choice | Use nodeId plus an available live or proposed choice, without replacement payload or revision | REST, MCP, admin |
23
+ | `flow_id_mismatch` | 400 | Resolution tried to change the proposal flow id | Keep the same flowId or submit a new proposal | REST, MCP |
16
24
  | `access_link_missing` | 401 | No Bearer / path token | Ask the user for a new access link of the right kind | GET, REST, MCP |
17
25
  | `bearer_scheme_required` | 401 | Auth used Basic (or non-Bearer) | Send `Authorization: Bearer <token>` | REST, MCP |
18
26
  | `token_expired` | 401 | Known token past `expiresAt` (body includes `expiredAt` and `accessLinksUrl`) | Ask the user to create a new link on the `/ai-access` dashboard → Access tokens (`accessLinksUrl`) | GET, REST, MCP |
@@ -38,10 +46,10 @@ GET-link `401` responses include a `WWW-Authenticate: Bearer …` header.
38
46
  | `invalid_diff_format` | 400 | Unknown `format` on proposal diff | Use `format=summary` (default) or `format=full` | GET, REST, MCP |
39
47
  | `confirm_required` | 400 | GET withdraw without `confirm=1` | Repeat the request with `confirm=1` | GET |
40
48
  | `missing_expected_rev` | 400 | Proposal submitted without `expectedRev` / `rev` | Read the flow and pass its `rev` | GET, REST, MCP |
41
- | `version_mismatch` | 409 | Stale `rev` / `expectedRev` | Re-read flows and resubmit | GET, REST, MCP |
49
+ | `version_mismatch` | 409 | Unknown or unavailable `rev` / `expectedRev` snapshot | Re-read flows and resubmit | GET, REST, MCP |
42
50
  | `proposal_not_found` | 404 | Unknown proposal id | List proposals and use an existing id | GET, REST, MCP |
43
- | `proposal_not_pending` | 409 | Action needs a pending proposal | Only pending can withdraw / decide | GET, REST, MCP |
44
- | `proposal_not_owned` | 403 | Withdraw of another token’s proposal | Only the creating token can withdraw | GET, REST, MCP |
51
+ | `proposal_not_pending` | 409 | Action needs an open proposal | Only pending or conflict proposals can withdraw / decide | GET, REST, MCP |
52
+ | `proposal_not_owned` | 403 | Resolve or withdraw another token’s proposal | Use the creating token; lost-token withdrawal requires a full admin session | GET, REST, MCP |
45
53
  | `validation_failed` | 400 | Several validation errors at once (message `N validation errors`) | Fix every entry in errors[] and resubmit. | GET, REST, MCP |
46
54
  | `duplicate_node_id` | 400 | The same node id appears more than once in a full tab or batch | Give every node a unique id | GET, REST, MCP |
47
55
  | `missing_node_id` | 400 | Node / op without an id | Include a non-empty string id | GET, REST, MCP |
@@ -72,6 +80,8 @@ Returned in `warnings[]` on a successful propose/submit; they never block.
72
80
 
73
81
  | Code | HTTP | Meaning | Hint / next step | Channels |
74
82
  | --- | --- | --- | --- | --- |
83
+ | `duplicate_env_keys` | 200 | Changed env arrays contain duplicate names; pairing by name is ambiguous when entries are reordered | Review the env values before approval | GET, REST, MCP, admin |
84
+ | `secret_may_revert` | 200 | Secret spans could not be aligned during proposed conflict resolution; proposed values were kept | Review the warned field against live flows before approval; an editor secret change may be reverted | GET, REST, MCP, admin |
75
85
  | `missing_config_node` | 201 | Warning: node has no (or an unresolved) config node yet | Configure before deploy if needed; see `candidates` | GET, REST, MCP |
76
86
  | `unwired_node` | 201 | Warning: node has outputs but no wires | Wire it, or ignore while scaffolding | GET, REST, MCP |
77
87
  | `incomplete_wires` | 201 | Warning: fewer wire ports than outputs | Add ports if outputs should connect | GET, REST, MCP |
@@ -83,9 +93,10 @@ Returned in `warnings[]` on a successful propose/submit; they never block.
83
93
  | `secret_tab_changed` | 201 | Warning: proposal with restored secrets also changes other nodes in the tab | Review every change before approving | GET, REST, MCP |
84
94
  | `secret_reference_added` | 201 | Warning: an added/changed node now reads a secret-named env/global/flow value (env.get, $env, ${…}, typed input) | Reviewer must check the value is not sent anywhere unexpected | GET, REST, MCP |
85
95
 
86
- ## Auto-closed proposal statuses
96
+ ## Proposal statuses
87
97
 
88
98
  | Code | HTTP | Meaning | Hint / next step | Channels |
89
99
  | --- | --- | --- | --- | --- |
90
- | `expired` | 200 | Status: pending proposal auto-closed because the live flow rev moved past `expectedRev` | Re-read the flow and submit a new proposal | GET, REST, MCP |
91
- | `superseded` | 200 | Status: pending proposal auto-closed because another proposal on the same flow was approved | Re-read the flow and submit a new proposal if still needed | GET, REST, MCP |
100
+ | `conflict` | 200 | Status: open proposal has conflicting editor changes; approval blocked | Read the current flow and resolve the proposal | GET, REST, MCP |
101
+ | `expired` | 200 | Status: open proposal expired after the configured TTL (default 7 days) | Re-read the flow and submit a new proposal | GET, REST, MCP |
102
+ | `superseded` | 200 | Legacy closed status; new approvals rebase other open proposals | Re-read the flow and submit a new proposal if still needed | GET, REST, MCP |