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

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/dist/plugin.d.cts CHANGED
@@ -217,7 +217,29 @@ declare class AuditLog {
217
217
  * Proposal JSON store under `.ai-access/proposals/`.
218
218
  */
219
219
 
220
- type ProposalStatus = 'pending' | 'approved' | 'rejected' | 'failed' | 'withdrawn' | 'expired' | 'superseded';
220
+ type ProposalStatus = 'pending' | 'conflict' | 'approved' | 'rejected' | 'failed' | 'withdrawn' | 'expired' | 'superseded';
221
+ /** One stored rebase conflict (field names only — never values). */
222
+ interface StoredRebaseConflict {
223
+ kind: string;
224
+ nodeId?: string;
225
+ nodeType?: string;
226
+ nodeName?: string;
227
+ fields?: string[];
228
+ message: string;
229
+ choices: ('live' | 'proposed')[];
230
+ }
231
+ /** Result of the last auto-rebase check against the live flows. */
232
+ interface ProposalRebaseInfo {
233
+ /** Live rev the check ran against. */
234
+ rev: string;
235
+ state: 'clean' | 'merged' | 'conflict';
236
+ /** Hash of the tab approve would deploy (absent on conflict). */
237
+ mergedHash?: string;
238
+ conflicts: StoredRebaseConflict[];
239
+ /** Nodes (or the tab) changed live since the base snapshot. */
240
+ liveChangedCount: number;
241
+ checkedAt: string;
242
+ }
221
243
  type ProposalType = 'replace_flow';
222
244
  interface AccessLinkProposalCreator {
223
245
  kind: 'access-link';
@@ -273,6 +295,14 @@ interface Proposal {
273
295
  nodeId: string;
274
296
  property: string;
275
297
  }[];
298
+ /** Persisted while a timed-out runtime deployment has an unknown outcome. */
299
+ deployPending?: {
300
+ mergedHash: string;
301
+ reason: string;
302
+ queueReleased?: boolean;
303
+ };
304
+ /** Last auto-rebase result (pending / conflict proposals). */
305
+ rebase?: ProposalRebaseInfo;
276
306
  /**
277
307
  * Short human title (≤80 chars). Set at submit (AI-supplied or auto-generated).
278
308
  * Optional on legacy proposals; API responses always resolve a title.
@@ -329,15 +359,38 @@ declare class ProposalStore {
329
359
  */
330
360
  beginApprove(id: string): Proposal;
331
361
  releaseLock(id: string): void;
362
+ recordDeployPending(id: string, pending: Proposal['deployPending']): Proposal;
332
363
  markApproved(id: string, decidedBy: ProposalDecisionBy, resultRev: string): Proposal;
333
364
  markRejected(id: string, decidedBy: ProposalDecisionBy | undefined, reason: string): Proposal;
334
365
  markFailed(id: string, decidedBy: ProposalDecisionBy | undefined, reason: string): Proposal;
335
366
  markWithdrawn(id: string, decidedBy: ProposalDecisionBy, reason: string): Proposal;
336
367
  /**
337
368
  * Auto-close a pending proposal that can no longer be approved
338
- * (`expired` = live rev moved; `superseded` = another proposal was approved).
369
+ * (`expired` = pending longer than the proposal TTL).
339
370
  */
340
371
  markClosed(id: string, status: 'expired' | 'superseded', reason: string, decidedBy?: ProposalDecisionBy): Proposal;
372
+ /**
373
+ * Store the latest rebase check and flip `pending` ↔ `conflict`.
374
+ * No-op (no write) when nothing changed. Skips decided or in-flight records.
375
+ */
376
+ recordRebase(id: string, info: ProposalRebaseInfo): Proposal;
377
+ /**
378
+ * Replace an open proposal's base snapshot and/or body (conflict resolution).
379
+ * Status returns to `pending`; the caller re-runs the rebase.
380
+ */
381
+ updateBody(id: string, update: {
382
+ baseFlow: Partial<FlowTab>;
383
+ flow: Partial<FlowTab>;
384
+ expectedRev?: string;
385
+ title?: string;
386
+ restoredSecrets?: {
387
+ nodeId: string;
388
+ property: string;
389
+ }[];
390
+ warnings?: ProposalWarning[];
391
+ }): Proposal;
392
+ /** True while an approve holds this proposal's lock. */
393
+ isApproving(id: string): boolean;
341
394
  }
342
395
 
343
396
  /**
@@ -357,6 +410,7 @@ interface PersistedAiAccessSettings {
357
410
  enabled?: boolean;
358
411
  allowWithoutAdminAuth?: boolean;
359
412
  defaultLinkTtlSeconds?: number;
413
+ proposalTtlDays?: number;
360
414
  defaultDeploymentType?: DeploymentType;
361
415
  maxActiveLinks?: number;
362
416
  publicBaseUrl?: string;
@@ -378,6 +432,7 @@ interface EffectiveAiAccessSettings {
378
432
  enabled: boolean;
379
433
  allowWithoutAdminAuth: boolean;
380
434
  defaultLinkTtlSeconds: number;
435
+ proposalTtlDays: number;
381
436
  defaultDeploymentType: DeploymentType;
382
437
  maxActiveLinks: number;
383
438
  publicBaseUrl: string;
@@ -463,15 +518,32 @@ type CatalogueFetcher = (url: string, etag?: string) => Promise<{
463
518
  * Approve / reject proposals through the FlowAdapter with audit logging.
464
519
  */
465
520
 
521
+ interface ProposalServiceOptions {
522
+ /** Maximum time to await updateFlow (default 60 seconds). Does not cancel the runtime operation. */
523
+ deployTimeoutMs?: number;
524
+ /** Maximum time waiting to start approval (default 60 seconds). */
525
+ approveQueueTimeoutMs?: number;
526
+ /** Release the queue after this much unsettled deployment time (default 5 minutes). */
527
+ deployHardCapMs?: number;
528
+ }
529
+ interface ApproveOptions {
530
+ deploymentType?: DeploymentType;
531
+ /**
532
+ * `mergedHash` the reviewer last saw. Required when the rebase merged live
533
+ * changes into the proposal; when supplied it must match the current merge.
534
+ */
535
+ mergedHash?: string;
536
+ }
466
537
  declare class ProposalService {
467
538
  private readonly proposals;
468
539
  private readonly audit;
469
540
  private readonly adapter;
470
541
  private readonly getDeploymentType;
471
- constructor(proposals: ProposalStore, audit: AuditLog, adapter: FlowAdapter, getDeploymentType: () => DeploymentType);
472
- approve(id: string, decidedBy: ProposalDecisionBy, options?: {
473
- deploymentType?: DeploymentType;
474
- }): Promise<Proposal>;
542
+ private readonly serviceOptions;
543
+ constructor(proposals: ProposalStore, audit: AuditLog, adapter: FlowAdapter, getDeploymentType: () => DeploymentType, serviceOptions?: ProposalServiceOptions);
544
+ approve(id: string, decidedBy: ProposalDecisionBy, options?: ApproveOptions): Promise<Proposal>;
545
+ private approveLocked;
546
+ private withDeployDeadline;
475
547
  reject(id: string, decidedBy: ProposalDecisionBy, reason: string): Proposal;
476
548
  }
477
549
 
@@ -495,10 +567,17 @@ interface AiAccessSettings {
495
567
  allowWithoutAdminAuth?: boolean;
496
568
  /** Default access-link TTL in seconds (default: 14400 = 4h). */
497
569
  defaultLinkTtlSeconds?: number;
570
+ proposalTtlDays?: number;
498
571
  /** Maximum concurrent active access links (default: 10). */
499
572
  maxActiveLinks?: number;
500
573
  /** Deployment type used when approving proposals (default: nodes). */
501
574
  defaultDeploymentType?: 'nodes' | 'flows' | 'full';
575
+ /** Approval queue waiting deadline in milliseconds (default 60,000). */
576
+ approveQueueTimeoutMs?: number;
577
+ /** Deployment caller deadline in milliseconds (default 60,000). */
578
+ deployTimeoutMs?: number;
579
+ /** Release an unsettled deployment's approval queue after this duration (default 300,000 ms). */
580
+ deployHardCapMs?: number;
502
581
  /**
503
582
  * Rate-limit window in milliseconds for `/mcp/api` and `/mcp/a`
504
583
  * (default: 60000).
@@ -643,6 +722,7 @@ interface NodeRedPluginAPI {
643
722
  };
644
723
  events?: {
645
724
  on(event: string, listener: () => void): void;
725
+ removeListener?(event: string, listener: () => void): void;
646
726
  };
647
727
  auth: NodeRedAuthAPI;
648
728
  plugins: {
@@ -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,10 +184,10 @@ 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
+ Conflicts block approval until resolved. Proposals expire only after the
189
+ configured `proposalTtlDays` (default 7). Approval rebases other open proposals;
190
+ legacy expired, rejected and superseded proposals stay closed.
191
191
 
192
192
  ## Withdraw
193
193
 
@@ -963,10 +963,9 @@ paths:
963
963
  flow tab. Does not mutate Node-RED until an admin with `*` approves.
964
964
 
965
965
  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).
966
+ from `GET /flows` or `GET /flows/{id}`. Recent known stale revisions are
967
+ accepted using snapshot history and rebased immediately. Unknown revisions
968
+ return **409** `version_mismatch`. Approval rechecks against the live revision.
970
969
 
971
970
  Optional `title` (≤80 chars) is stored as a human-friendly label; when
972
971
  omitted it is auto-generated from the change set. Responses always
@@ -1055,14 +1054,14 @@ paths:
1055
1054
  (long text fields listed for expandable UI). Modified nodes include
1056
1055
  line-level diffs for text fields such as `func` and `template`.
1057
1056
 
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.
1057
+ `baseRevStale` is true only for **open** proposals when
1058
+ `expectedRev` does not match the live revision (informational).
1059
+ Closed proposals never set `baseRevStale`. Open proposals include
1060
+ `rebase` and `liveChanges` (editor changes since the stored base).
1061
+ Without conflicts, `vsCurrent` compares the merged deployment to live.
1062
+ Merges use raw stored proposals and live flows; AI diff sections are redacted.
1063
1063
 
1064
- Admins with `read` can diff any proposal. Access-link clients can only
1065
- diff proposals they created.
1064
+ Admins with `read` and access-link clients can diff any proposal.
1066
1065
  operationId: getProposalDiff
1067
1066
  security:
1068
1067
  - AccessLinkBearer: []
@@ -1085,6 +1084,97 @@ paths:
1085
1084
  '404':
1086
1085
  $ref: '#/components/responses/NotFound'
1087
1086
 
1087
+ /proposals/{id}/resolve-conflict:
1088
+ post:
1089
+ tags: [Proposals]
1090
+ summary: Choose live or proposed for one current conflict
1091
+ operationId: resolveProposalConflict
1092
+ security:
1093
+ - AdminSession: []
1094
+ parameters:
1095
+ - $ref: '#/components/parameters/ProposalId'
1096
+ requestBody:
1097
+ required: true
1098
+ content:
1099
+ application/json:
1100
+ schema:
1101
+ $ref: '#/components/schemas/ConflictChoiceRequest'
1102
+ responses:
1103
+ '200':
1104
+ description: Presented proposal with current rebase metadata
1105
+ content:
1106
+ application/json:
1107
+ schema:
1108
+ $ref: '#/components/schemas/Proposal'
1109
+ '400':
1110
+ $ref: '#/components/responses/BadRequest'
1111
+ '401':
1112
+ $ref: '#/components/responses/Unauthorized'
1113
+ '403':
1114
+ $ref: '#/components/responses/Forbidden'
1115
+ '404':
1116
+ description: Proposal not found
1117
+ '409':
1118
+ description: Proposal closed, conflict missing, or revision unavailable
1119
+ /proposals/{id}/recheck:
1120
+ post:
1121
+ tags: [Proposals]
1122
+ summary: Recheck an open proposal against raw live flows
1123
+ operationId: recheckProposal
1124
+ security:
1125
+ - AdminSession: []
1126
+ parameters:
1127
+ - $ref: '#/components/parameters/ProposalId'
1128
+ responses:
1129
+ '200':
1130
+ description: Presented proposal with current rebase metadata
1131
+ content:
1132
+ application/json:
1133
+ schema:
1134
+ $ref: '#/components/schemas/Proposal'
1135
+ '400':
1136
+ $ref: '#/components/responses/BadRequest'
1137
+ '401':
1138
+ $ref: '#/components/responses/Unauthorized'
1139
+ '403':
1140
+ $ref: '#/components/responses/Forbidden'
1141
+ '404':
1142
+ description: Proposal not found
1143
+ '409':
1144
+ description: Proposal closed, conflict missing, or revision unavailable
1145
+ /proposals/{id}/resolve:
1146
+ post:
1147
+ tags: [Proposals]
1148
+ summary: Resolve your own open proposal with a fresh payload
1149
+ operationId: resolveProposal
1150
+ security:
1151
+ - AccessLinkBearer: []
1152
+ parameters:
1153
+ - $ref: '#/components/parameters/ProposalId'
1154
+ requestBody:
1155
+ required: true
1156
+ content:
1157
+ application/json:
1158
+ schema:
1159
+ $ref: '#/components/schemas/ResolveProposalRequest'
1160
+ responses:
1161
+ '200':
1162
+ description: Presented proposal with current rebase metadata
1163
+ content:
1164
+ application/json:
1165
+ schema:
1166
+ $ref: '#/components/schemas/Proposal'
1167
+ '400':
1168
+ $ref: '#/components/responses/BadRequest'
1169
+ '401':
1170
+ $ref: '#/components/responses/Unauthorized'
1171
+ '403':
1172
+ $ref: '#/components/responses/Forbidden'
1173
+ '404':
1174
+ description: Proposal not found
1175
+ '409':
1176
+ description: Proposal closed, conflict missing, or revision unavailable
1177
+
1088
1178
  /proposals/{id}/approve:
1089
1179
  post:
1090
1180
  tags: [Proposals]
@@ -1094,9 +1184,12 @@ paths:
1094
1184
  Optional body `deploymentType`: `nodes` (default from settings),
1095
1185
  `flows`, or `full`.
1096
1186
 
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.
1187
+ Auto-rebases against raw live flows and deploys the merged tab with the live rev.
1188
+ Conflicts return 409 `proposal_conflict` with field-name-only `conflicts`.
1189
+ Merged editor changes require `mergedHash` from the reviewed diff; otherwise
1190
+ 409 `rebase_ack_required` includes the current hash. Revision races retry
1191
+ up to three times then return 409 `rebase_race`, leaving the proposal open.
1192
+ Other deploy errors mark it `failed`. Concurrent approval returns 409.
1100
1193
  operationId: approveProposal
1101
1194
  security:
1102
1195
  - AdminSession: []
@@ -1181,7 +1274,7 @@ paths:
1181
1274
  tags: [Proposals]
1182
1275
  summary: Withdraw own pending proposal
1183
1276
  description: |
1184
- Access-link Bearer only. Withdraws a **pending** proposal created by
1277
+ Access-link Bearer only. Withdraws an **open** (pending or conflict) proposal created by
1185
1278
  this token. Status becomes `withdrawn` (not `rejected`). Optional
1186
1279
  JSON body `{ "reason": "…" }`.
1187
1280
  operationId: withdrawProposal
@@ -1369,8 +1462,8 @@ components:
1369
1462
  code: access_link_invalid
1370
1463
  VersionConflict:
1371
1464
  description: |
1372
- `expectedRev` does not match the current flow revision
1373
- (`code: version_mismatch`). Re-read flows and resubmit.
1465
+ `expectedRev` is unavailable in recent snapshot history and does not
1466
+ match the live revision (`code: version_mismatch`). Re-read flows and resubmit.
1374
1467
  content:
1375
1468
  application/json:
1376
1469
  schema:
@@ -1456,6 +1549,14 @@ components:
1456
1549
  description: |
1457
1550
  Machine-readable `code` on JSON error bodies. See docs/errors.md.
1458
1551
 
1552
+ - `approve_queue_timeout` (504): Approval exceeded its queue-wait deadline (default 60 seconds); no deployment was started for this request
1553
+ - `deploy_timeout` (504): Deployment exceeded its deadline (default 60 seconds); approvals wait for settlement or the hard cap (default 5 minutes)
1554
+ - `proposal_conflict` (409): Editor and proposal changes conflict (conflicts contains field names only)
1555
+ - `rebase_ack_required` (409): The merged result needs review (mergedHash included)
1556
+ - `rebase_race` (409): Flows kept changing during approval
1557
+ - `conflict_not_found` (409): The requested conflict no longer exists
1558
+ - `invalid_choice` (400): Invalid manual conflict choice
1559
+ - `flow_id_mismatch` (400): Resolution tried to change the proposal flow id
1459
1560
  - `access_link_missing` (401): No Bearer / path token
1460
1561
  - `bearer_scheme_required` (401): Auth used Basic (or non-Bearer)
1461
1562
  - `token_expired` (401): Known token past `expiresAt` (body includes `expiredAt` and `accessLinksUrl`)
@@ -1481,9 +1582,9 @@ components:
1481
1582
  - `invalid_diff_format` (400): Unknown `format` on proposal diff
1482
1583
  - `confirm_required` (400): GET withdraw without `confirm=1`
1483
1584
  - `missing_expected_rev` (400): Proposal submitted without `expectedRev` / `rev`
1484
- - `version_mismatch` (409): Stale `rev` / `expectedRev`
1585
+ - `version_mismatch` (409): Unknown or unavailable `rev` / `expectedRev` snapshot
1485
1586
  - `proposal_not_found` (404): Unknown proposal id
1486
- - `proposal_not_pending` (409): Action needs a pending proposal
1587
+ - `proposal_not_pending` (409): Action needs an open proposal
1487
1588
  - `proposal_not_owned` (403): Withdraw of another token’s proposal
1488
1589
  - `validation_failed` (400): Several validation errors at once (message `N validation errors`)
1489
1590
  - `duplicate_node_id` (400): The same node id appears more than once in a full tab or batch
@@ -1509,6 +1610,14 @@ components:
1509
1610
  - `upstream_error` (502): Node-RED runtime/flows error
1510
1611
  - `internal_error` (500): Unexpected server error
1511
1612
  enum:
1613
+ - approve_queue_timeout
1614
+ - deploy_timeout
1615
+ - proposal_conflict
1616
+ - rebase_ack_required
1617
+ - rebase_race
1618
+ - conflict_not_found
1619
+ - invalid_choice
1620
+ - flow_id_mismatch
1512
1621
  - access_link_missing
1513
1622
  - bearer_scheme_required
1514
1623
  - token_expired
@@ -1737,6 +1846,11 @@ components:
1737
1846
  AiAccessSettingsPatch:
1738
1847
  type: object
1739
1848
  properties:
1849
+ proposalTtlDays:
1850
+ type: integer
1851
+ minimum: 1
1852
+ maximum: 365
1853
+ default: 7
1740
1854
  enabled:
1741
1855
  type: boolean
1742
1856
  allowWithoutAdminAuth:
@@ -2184,9 +2298,46 @@ components:
2184
2298
  additionalProperties: true
2185
2299
  additionalProperties: true
2186
2300
 
2301
+ ProposalRebase:
2302
+ type: object
2303
+ description: Merge metadata; conflicts contain field names only, never field values.
2304
+ properties:
2305
+ state:
2306
+ type: string
2307
+ enum: [clean, merged, conflict]
2308
+ rev:
2309
+ type: string
2310
+ mergedHash:
2311
+ type: string
2312
+ conflicts:
2313
+ type: array
2314
+ items:
2315
+ type: object
2316
+ properties:
2317
+ nodeId:
2318
+ type: string
2319
+ kind:
2320
+ type: string
2321
+ fields:
2322
+ type: array
2323
+ items:
2324
+ type: string
2325
+ message:
2326
+ type: string
2327
+ choices:
2328
+ type: array
2329
+ items:
2330
+ type: string
2331
+ enum: [live, proposed]
2332
+ liveChangedCount:
2333
+ type: integer
2334
+ checkedAt:
2335
+ type: string
2336
+ format: date-time
2337
+
2187
2338
  ProposalStatus:
2188
2339
  type: string
2189
- enum: [pending, approved, rejected, failed, withdrawn, expired, superseded]
2340
+ enum: [pending, conflict, approved, rejected, failed, withdrawn, expired, superseded]
2190
2341
 
2191
2342
  Proposal:
2192
2343
  type: object
@@ -2204,6 +2355,20 @@ components:
2204
2355
  - reviewUrl
2205
2356
  - reviewPath
2206
2357
  properties:
2358
+ deployPending:
2359
+ type: object
2360
+ description: Timed-out deployment has an unknown outcome; approvals wait until settlement or the hard cap. Check live flows before approving again.
2361
+ required: [mergedHash, reason]
2362
+ properties:
2363
+ mergedHash:
2364
+ type: string
2365
+ reason:
2366
+ type: string
2367
+ queueReleased:
2368
+ type: boolean
2369
+ description: True when the deploy hard cap released the queue. Late settlement no longer updates this proposal; approval always re-reads live flows.
2370
+ rebase:
2371
+ $ref: '#/components/schemas/ProposalRebase'
2207
2372
  id:
2208
2373
  type: string
2209
2374
  type:
@@ -2318,9 +2483,8 @@ components:
2318
2483
  reason:
2319
2484
  type: string
2320
2485
  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.
2486
+ Present when rejected, failed, withdrawn, or expired. Revision changes
2487
+ keep open proposals pending or in conflict; they do not close them.
2324
2488
  resultRev:
2325
2489
  type: string
2326
2490
 
@@ -2424,9 +2588,39 @@ components:
2424
2588
  find: Hello
2425
2589
  replace: Hi
2426
2590
 
2591
+ ConflictChoiceRequest:
2592
+ type: object
2593
+ required: [nodeId, choice]
2594
+ properties:
2595
+ nodeId:
2596
+ type: string
2597
+ choice:
2598
+ type: string
2599
+ enum: [live, proposed]
2600
+
2601
+ ResolveProposalRequest:
2602
+ type: object
2603
+ required: [expectedRev]
2604
+ properties:
2605
+ expectedRev:
2606
+ type: string
2607
+ title:
2608
+ type: string
2609
+ maxLength: 80
2610
+ payload:
2611
+ $ref: '#/components/schemas/ReplaceFlowPayload'
2612
+ nodes:
2613
+ type: array
2614
+ items:
2615
+ type: object
2616
+ additionalProperties: true
2617
+
2427
2618
  ApproveProposalRequest:
2428
2619
  type: object
2429
2620
  properties:
2621
+ mergedHash:
2622
+ type: string
2623
+ description: Hash of the reviewed merged result; required when editor changes are merged.
2430
2624
  deploymentType:
2431
2625
  $ref: '#/components/schemas/DeploymentType'
2432
2626
 
@@ -2586,7 +2780,7 @@ components:
2586
2780
  baseRevStale:
2587
2781
  type: boolean
2588
2782
  description: |
2589
- True only for pending proposals when expectedRev ≠ live rev.
2783
+ Informational: true for open proposals when expectedRev ≠ live rev.
2590
2784
  Never true for approved/rejected/failed.
2591
2785
  compareAgainst:
2592
2786
  type: string
@@ -2594,10 +2788,14 @@ components:
2594
2788
  baseSnapshotMissing:
2595
2789
  type: boolean
2596
2790
  description: True for legacy proposals without a stored baseFlow
2791
+ rebase:
2792
+ $ref: '#/components/schemas/ProposalRebase'
2793
+ liveChanges:
2794
+ $ref: '#/components/schemas/ProposalDiffSections'
2597
2795
  vsCurrent:
2598
2796
  $ref: '#/components/schemas/ProposalDiffSections'
2599
2797
  description: |
2600
- Present when pending and baseRevStale — proposal vs live flow.
2798
+ Present for open proposals without conflicts — merged result vs live flow.
2601
2799
 
2602
2800
  AuditActor:
2603
2801
  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 | admin |
22
+ | `invalid_choice` | 400 | Invalid manual conflict choice | Use an available live or proposed choice | 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,9 +46,9 @@ 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 |
51
+ | `proposal_not_pending` | 409 | Action needs an open proposal | Only pending or conflict proposals can withdraw / decide | GET, REST, MCP |
44
52
  | `proposal_not_owned` | 403 | Withdraw of another token’s proposal | Only the creating token can withdraw | 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 |
@@ -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 |