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

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
@@ -218,7 +218,7 @@ declare class AuditLog {
218
218
  */
219
219
 
220
220
  type ProposalStatus = 'pending' | 'conflict' | 'approved' | 'rejected' | 'failed' | 'withdrawn' | 'expired' | 'superseded';
221
- /** One stored rebase conflict (field names only — never values). */
221
+ /** One stored rebase conflict (field paths only, including env names / credential keys; never values). */
222
222
  interface StoredRebaseConflict {
223
223
  kind: string;
224
224
  nodeId?: string;
@@ -228,6 +228,11 @@ interface StoredRebaseConflict {
228
228
  message: string;
229
229
  choices: ('live' | 'proposed')[];
230
230
  }
231
+ /** Paths where an AI conflict choice replaced a live editor change. */
232
+ interface AiConflictOverride {
233
+ nodeId: string;
234
+ fields: string[];
235
+ }
231
236
  /** Result of the last auto-rebase check against the live flows. */
232
237
  interface ProposalRebaseInfo {
233
238
  /** Live rev the check ran against. */
@@ -295,14 +300,33 @@ interface Proposal {
295
300
  nodeId: string;
296
301
  property: string;
297
302
  }[];
303
+ deploymentAlert?: string;
304
+ landedAfterWithdraw?: boolean;
305
+ rejectedWhileDeployUnknown?: boolean;
298
306
  /** Persisted while a timed-out runtime deployment has an unknown outcome. */
299
307
  deployPending?: {
300
308
  mergedHash: string;
309
+ bootId?: string;
301
310
  reason: string;
302
311
  queueReleased?: boolean;
312
+ attemptId?: string;
303
313
  };
304
314
  /** Last auto-rebase result (pending / conflict proposals). */
305
315
  rebase?: ProposalRebaseInfo;
316
+ /** AI resolutions that replace editor changes and still require reviewer acknowledgement. */
317
+ aiConflictOverrides?: AiConflictOverride[];
318
+ /** Latest manual resolution, retained for admin undo until the next body edit. */
319
+ conflictChoice?: {
320
+ nodeId: string;
321
+ choice: 'live' | 'proposed';
322
+ chooser?: 'ai' | 'human';
323
+ undo?: {
324
+ baseFlow: Partial<FlowTab>;
325
+ flow: Partial<FlowTab>;
326
+ warnings: ProposalWarning[];
327
+ aiConflictOverrides?: AiConflictOverride[];
328
+ };
329
+ };
306
330
  /**
307
331
  * Short human title (≤80 chars). Set at submit (AI-supplied or auto-generated).
308
332
  * Optional on legacy proposals; API responses always resolve a title.
@@ -347,6 +371,7 @@ declare class ProposalStore {
347
371
  private toDiskRecord;
348
372
  private writeAtomic;
349
373
  create(input: CreateProposalInput): Proposal;
374
+ private currentProcessProposal;
350
375
  get(id: string): Proposal;
351
376
  list(filter?: {
352
377
  linkId?: string;
@@ -360,6 +385,9 @@ declare class ProposalStore {
360
385
  beginApprove(id: string): Proposal;
361
386
  releaseLock(id: string): void;
362
387
  recordDeployPending(id: string, pending: Proposal['deployPending']): Proposal;
388
+ flagRejectedWhileDeployUnknown(id: string): Proposal;
389
+ flagLandedAfterWithdraw(id: string): void;
390
+ flagDeployment(id: string, message: string): void;
363
391
  markApproved(id: string, decidedBy: ProposalDecisionBy, resultRev: string): Proposal;
364
392
  markRejected(id: string, decidedBy: ProposalDecisionBy | undefined, reason: string): Proposal;
365
393
  markFailed(id: string, decidedBy: ProposalDecisionBy | undefined, reason: string): Proposal;
@@ -388,6 +416,8 @@ declare class ProposalStore {
388
416
  property: string;
389
417
  }[];
390
418
  warnings?: ProposalWarning[];
419
+ conflictChoice?: Proposal['conflictChoice'];
420
+ aiConflictOverrides?: AiConflictOverride[];
391
421
  }): Proposal;
392
422
  /** True while an approve holds this proposal's lock. */
393
423
  isApproving(id: string): boolean;
@@ -527,6 +557,7 @@ interface ProposalServiceOptions {
527
557
  deployHardCapMs?: number;
528
558
  }
529
559
  interface ApproveOptions {
560
+ overrideDeployOutcomeUnknown?: boolean;
530
561
  deploymentType?: DeploymentType;
531
562
  /**
532
563
  * `mergedHash` the reviewer last saw. Required when the rebase merged live
@@ -541,6 +572,9 @@ declare class ProposalService {
541
572
  private readonly getDeploymentType;
542
573
  private readonly serviceOptions;
543
574
  constructor(proposals: ProposalStore, audit: AuditLog, adapter: FlowAdapter, getDeploymentType: () => DeploymentType, serviceOptions?: ProposalServiceOptions);
575
+ unknownDeployBlocks(): Proposal[];
576
+ confirmLiveChecked(id: string, actor: ProposalDecisionBy, expectedAttemptId: unknown): Proposal;
577
+ private guardUnknown;
544
578
  approve(id: string, decidedBy: ProposalDecisionBy, options?: ApproveOptions): Promise<Proposal>;
545
579
  private approveLocked;
546
580
  private withDeployDeadline;
@@ -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.9
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
@@ -185,6 +185,10 @@ info:
185
185
  full tab or batch return `duplicate_node_id`.
186
186
 
187
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.
188
192
  Conflicts block approval until resolved. Proposals expire only after the
189
193
  configured `proposalTtlDays` (default 7). Approval rebases other open proposals;
190
194
  legacy expired, rejected and superseded proposals stay closed.
@@ -194,7 +198,11 @@ info:
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
@@ -942,6 +950,21 @@ paths:
942
950
  type: array
943
951
  items:
944
952
  $ref: '#/components/schemas/Proposal'
953
+ deployUnknownBlocks:
954
+ type: array
955
+ description: Admin-only active process blocks, independent of status filters and pagination. Each deployment submits a full flow snapshot, so these blocks apply to all approvals, including after proposal closure.
956
+ items:
957
+ type: object
958
+ required: [proposalId, flowIds]
959
+ properties:
960
+ proposalId:
961
+ type: string
962
+ attemptId:
963
+ type: string
964
+ flowIds:
965
+ type: array
966
+ items:
967
+ type: string
945
968
  total:
946
969
  type: integer
947
970
  description: Count after filters, before limit/offset
@@ -1054,9 +1077,13 @@ paths:
1054
1077
  (long text fields listed for expandable UI). Modified nodes include
1055
1078
  line-level diffs for text fields such as `func` and `template`.
1056
1079
 
1057
- `baseRevStale` is true only for **open** proposals when
1080
+ `baseRev` aliases `expectedRev`, the stored proposal base revision.
1081
+ `baseRevChanged` is true only for **open** proposals when
1058
1082
  `expectedRev` does not match the live revision (informational).
1059
- Closed proposals never set `baseRevStale`. Open proposals include
1083
+ This is informational after a successful rebase, not an approval error.
1084
+ `rebase.state` (`clean`, `merged`, `conflict`) reports merge readiness and
1085
+ `rebase.rev` identifies the checked live rev. Closed proposals never set
1086
+ `baseRevChanged` as true. Open proposals include
1060
1087
  `rebase` and `liveChanges` (editor changes since the stored base).
1061
1088
  Without conflicts, `vsCurrent` compares the merged deployment to live.
1062
1089
  Merges use raw stored proposals and live flows; AI diff sections are redacted.
@@ -1087,7 +1114,7 @@ paths:
1087
1114
  /proposals/{id}/resolve-conflict:
1088
1115
  post:
1089
1116
  tags: [Proposals]
1090
- summary: Choose live or proposed for one current conflict
1117
+ summary: Choose live or proposed for one current conflict, or undo the latest choice
1091
1118
  operationId: resolveProposalConflict
1092
1119
  security:
1093
1120
  - AdminSession: []
@@ -1145,7 +1172,12 @@ paths:
1145
1172
  /proposals/{id}/resolve:
1146
1173
  post:
1147
1174
  tags: [Proposals]
1148
- summary: Resolve your own open proposal with a fresh payload
1175
+ summary: Resolve your own open proposal with a choice or replacement payload
1176
+ description: |
1177
+ Use nodeId + choice for one current conflict (overlapping fields only;
1178
+ independent live and proposed edits survive). Or use expectedRev + payload:
1179
+ operations apply to the flow snapshot at expectedRev, not the stored
1180
+ proposal or merged result. Include every intended edit. Do not mix modes.
1149
1181
  operationId: resolveProposal
1150
1182
  security:
1151
1183
  - AccessLinkBearer: []
@@ -1227,6 +1259,42 @@ paths:
1227
1259
  '415':
1228
1260
  $ref: '#/components/responses/UnsupportedMediaType'
1229
1261
 
1262
+ /proposals/{id}/confirm-live-checked:
1263
+ post:
1264
+ tags: [Proposals]
1265
+ summary: Confirm live flows checked and clear the unknown deployment block
1266
+ description: Requires full admin permission (*). Records the actor and attempt in proposal.live_checked audit. Requires a post-cap deployPending.queueReleased flag and its matching attemptId. Does not cancel a detached deploy; a later landing is still monitored on every changed tab.
1267
+ operationId: confirmProposalLiveChecked
1268
+ security:
1269
+ - AdminSession: []
1270
+ parameters:
1271
+ - $ref: '#/components/parameters/ProposalId'
1272
+ requestBody:
1273
+ required: true
1274
+ content:
1275
+ application/json:
1276
+ schema:
1277
+ type: object
1278
+ required: [attemptId]
1279
+ properties:
1280
+ attemptId:
1281
+ type: string
1282
+ responses:
1283
+ '200':
1284
+ description: Block cleared
1285
+ content:
1286
+ application/json:
1287
+ schema:
1288
+ $ref: '#/components/schemas/Proposal'
1289
+ '401':
1290
+ $ref: '#/components/responses/Unauthorized'
1291
+ '403':
1292
+ $ref: '#/components/responses/Forbidden'
1293
+ '409':
1294
+ description: deploy_outcome_unknown — no post-cap deployment or attemptId does not match
1295
+ '404':
1296
+ $ref: '#/components/responses/NotFound'
1297
+
1230
1298
  /proposals/{id}/reject:
1231
1299
  post:
1232
1300
  tags: [Proposals]
@@ -1272,14 +1340,16 @@ paths:
1272
1340
  /proposals/{id}/withdraw:
1273
1341
  post:
1274
1342
  tags: [Proposals]
1275
- summary: Withdraw own pending proposal
1343
+ summary: Withdraw an open proposal (owner token or full admin recovery)
1276
1344
  description: |
1277
- Access-link Bearer only. Withdraws an **open** (pending or conflict) proposal created by
1278
- this token. Status becomes `withdrawn` (not `rejected`). Optional
1345
+ Accepts the creating access-link Bearer, or a full (`*`) admin session
1346
+ for lost-token recovery. Other tokens and read-only admins are forbidden.
1347
+ Withdraws an **open** (pending or conflict) proposal. Status becomes `withdrawn` (not `rejected`). Optional
1279
1348
  JSON body `{ "reason": "…" }`.
1280
1349
  operationId: withdrawProposal
1281
1350
  security:
1282
1351
  - AccessLinkBearer: []
1352
+ - AdminSession: []
1283
1353
  parameters:
1284
1354
  - $ref: '#/components/parameters/ProposalId'
1285
1355
  requestBody:
@@ -1301,7 +1371,7 @@ paths:
1301
1371
  '401':
1302
1372
  $ref: '#/components/responses/Unauthorized'
1303
1373
  '403':
1304
- description: Not owned by this access link
1374
+ description: Not owned by this access link or admin lacks full permission
1305
1375
  content:
1306
1376
  application/json:
1307
1377
  schema:
@@ -1550,6 +1620,7 @@ components:
1550
1620
  Machine-readable `code` on JSON error bodies. See docs/errors.md.
1551
1621
 
1552
1622
  - `approve_queue_timeout` (504): Approval exceeded its queue-wait deadline (default 60 seconds); no deployment was started for this request
1623
+ - `deploy_outcome_unknown` (409): A past-cap deployment can still change live flows
1553
1624
  - `deploy_timeout` (504): Deployment exceeded its deadline (default 60 seconds); approvals wait for settlement or the hard cap (default 5 minutes)
1554
1625
  - `proposal_conflict` (409): Editor and proposal changes conflict (conflicts contains field names only)
1555
1626
  - `rebase_ack_required` (409): The merged result needs review (mergedHash included)
@@ -1585,7 +1656,7 @@ components:
1585
1656
  - `version_mismatch` (409): Unknown or unavailable `rev` / `expectedRev` snapshot
1586
1657
  - `proposal_not_found` (404): Unknown proposal id
1587
1658
  - `proposal_not_pending` (409): Action needs an open proposal
1588
- - `proposal_not_owned` (403): Withdraw of another token’s proposal
1659
+ - `proposal_not_owned` (403): Resolve or withdraw another token’s proposal
1589
1660
  - `validation_failed` (400): Several validation errors at once (message `N validation errors`)
1590
1661
  - `duplicate_node_id` (400): The same node id appears more than once in a full tab or batch
1591
1662
  - `missing_node_id` (400): Node / op without an id
@@ -1611,6 +1682,7 @@ components:
1611
1682
  - `internal_error` (500): Unexpected server error
1612
1683
  enum:
1613
1684
  - approve_queue_timeout
1685
+ - deploy_outcome_unknown
1614
1686
  - deploy_timeout
1615
1687
  - proposal_conflict
1616
1688
  - rebase_ack_required
@@ -2346,6 +2418,7 @@ components:
2346
2418
  - type
2347
2419
  - status
2348
2420
  - expectedRev
2421
+ - baseRev
2349
2422
  - payload
2350
2423
  - createdBy
2351
2424
  - createdAt
@@ -2355,18 +2428,42 @@ components:
2355
2428
  - reviewUrl
2356
2429
  - reviewPath
2357
2430
  properties:
2431
+ conflictChoice:
2432
+ type: object
2433
+ description: Latest manual resolution; admins can undo it until the next body edit or choice. Undo snapshots are never returned.
2434
+ required: [nodeId, choice]
2435
+ properties:
2436
+ nodeId:
2437
+ type: string
2438
+ choice:
2439
+ type: string
2440
+ enum: [live, proposed]
2441
+ rejectedWhileDeployUnknown:
2442
+ type: boolean
2443
+ description: Rejected while a full-snapshot deploy could still land. Check live flows; late landing triggers an alert and audit entry.
2444
+ landedAfterWithdraw:
2445
+ type: boolean
2446
+ description: The withdrawn proposal’s outstanding deployment landed later; shown as a dashboard badge.
2447
+ deploymentAlert:
2448
+ type: string
2449
+ description: Warning that a late deployment may have overwritten an intervening decision.
2358
2450
  deployPending:
2359
2451
  type: object
2360
2452
  description: Timed-out deployment has an unknown outcome; approvals wait until settlement or the hard cap. Check live flows before approving again.
2361
2453
  required: [mergedHash, reason]
2362
2454
  properties:
2455
+ attemptId:
2456
+ type: string
2457
+ bootId:
2458
+ type: string
2459
+ description: Process identity; flags from previous processes are ignored.
2363
2460
  mergedHash:
2364
2461
  type: string
2365
2462
  reason:
2366
2463
  type: string
2367
2464
  queueReleased:
2368
2465
  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.
2466
+ description: True when the deploy hard cap released the queue. All approvals are blocked until settlement, restart, or confirm-live-checked with a matching attemptId; closing the proposal does not clear the block; approval may proceed with an audited override. Rejection is allowed with rejectedWhileDeployUnknown warning. Late settlement updates the proposal or flags intervening decisions.
2370
2467
  rebase:
2371
2468
  $ref: '#/components/schemas/ProposalRebase'
2372
2469
  id:
@@ -2378,6 +2475,12 @@ components:
2378
2475
  $ref: '#/components/schemas/ProposalStatus'
2379
2476
  expectedRev:
2380
2477
  type: string
2478
+ baseRev:
2479
+ type: string
2480
+ description: Revision of the stored base snapshot; alias of expectedRev.
2481
+ baseRevChanged:
2482
+ type: boolean
2483
+ description: Informational revision difference for open proposals; use rebase.state for merge readiness.
2381
2484
  payload:
2382
2485
  $ref: '#/components/schemas/ReplaceFlowPayload'
2383
2486
  baseFlow:
@@ -2536,7 +2639,7 @@ components:
2536
2639
  type: object
2537
2640
  required: [kind]
2538
2641
  description: |
2539
- Admin for approve/reject/fail; access-link for withdraw.
2642
+ Admin for approve/reject/fail and recovery withdrawal; access-link for owner withdrawal.
2540
2643
  properties:
2541
2644
  kind:
2542
2645
  type: string
@@ -2596,28 +2699,52 @@ components:
2596
2699
  type: string
2597
2700
  choice:
2598
2701
  type: string
2599
- enum: [live, proposed]
2702
+ enum: [live, proposed, undo]
2703
+ 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.
2600
2704
 
2601
2705
  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
2706
+ oneOf:
2707
+ - type: object
2708
+ additionalProperties: false
2709
+ required: [nodeId, choice]
2710
+ properties:
2711
+ nodeId:
2712
+ type: string
2713
+ description: Current conflict node id; flow id for tab/env conflicts.
2714
+ choice:
2715
+ type: string
2716
+ enum: [live, proposed]
2717
+ description: Keep this side for overlapping fields only; preserve independent changes and live secrets.
2718
+ - type: object
2719
+ required: [expectedRev]
2720
+ properties:
2721
+ expectedRev:
2722
+ type: string
2723
+ description: Flow snapshot revision to which replacement operations apply.
2724
+ title:
2725
+ type: string
2726
+ maxLength: 80
2727
+ payload:
2728
+ $ref: '#/components/schemas/ReplaceFlowPayload'
2729
+ description: Complete intended edits based on expectedRev; not applied to the stored proposal or merged result.
2730
+ nodes:
2731
+ type: array
2732
+ description: Legacy alias for replacement payload nodes, applied to expectedRev.
2733
+ items:
2734
+ type: object
2735
+ additionalProperties: true
2736
+ not:
2737
+ anyOf:
2738
+ - required: [nodeId]
2739
+ - required: [choice]
2617
2740
 
2618
2741
  ApproveProposalRequest:
2619
2742
  type: object
2620
2743
  properties:
2744
+ overrideDeployOutcomeUnknown:
2745
+ type: boolean
2746
+ default: false
2747
+ description: Explicitly allow approval while a past-cap deploy can still overwrite live flows.
2621
2748
  mergedHash:
2622
2749
  type: string
2623
2750
  description: Hash of the reviewed merged result; required when editor changes are merged.
@@ -2730,6 +2857,11 @@ components:
2730
2857
  type: object
2731
2858
  required: [summary, added, removed, modified]
2732
2859
  properties:
2860
+ tabProperties:
2861
+ type: array
2862
+ description: Before and after values of changed flow settings, including env. Uses the caller's redacted view.
2863
+ items:
2864
+ $ref: '#/components/schemas/ProposalDiffPropertyChange'
2733
2865
  summary:
2734
2866
  type: object
2735
2867
  required: [added, removed, modified, tabChanges]
@@ -2766,7 +2898,8 @@ components:
2766
2898
  - flowId
2767
2899
  - expectedRev
2768
2900
  - currentRev
2769
- - baseRevStale
2901
+ - baseRevChanged
2902
+ - baseRev
2770
2903
  - compareAgainst
2771
2904
  properties:
2772
2905
  proposalId:
@@ -2775,9 +2908,12 @@ components:
2775
2908
  type: string
2776
2909
  expectedRev:
2777
2910
  type: string
2911
+ baseRev:
2912
+ type: string
2913
+ description: Alias of expectedRev, the stored base revision.
2778
2914
  currentRev:
2779
2915
  type: string
2780
- baseRevStale:
2916
+ baseRevChanged:
2781
2917
  type: boolean
2782
2918
  description: |
2783
2919
  Informational: true for open proposals when expectedRev ≠ live rev.
@@ -2790,6 +2926,26 @@ components:
2790
2926
  description: True for legacy proposals without a stored baseFlow
2791
2927
  rebase:
2792
2928
  $ref: '#/components/schemas/ProposalRebase'
2929
+ conflictValues:
2930
+ type: array
2931
+ description: Base, live and proposed values for each conflicting path. AI callers receive redacted values.
2932
+ items:
2933
+ type: object
2934
+ required: [nodeId, fields]
2935
+ properties:
2936
+ nodeId:
2937
+ type: string
2938
+ fields:
2939
+ type: array
2940
+ items:
2941
+ type: object
2942
+ required: [field]
2943
+ properties:
2944
+ field:
2945
+ type: string
2946
+ base: {}
2947
+ live: {}
2948
+ proposed: {}
2793
2949
  liveChanges:
2794
2950
  $ref: '#/components/schemas/ProposalDiffSections'
2795
2951
  vsCurrent:
package/docs/errors.md CHANGED
@@ -14,12 +14,13 @@ GET-link `401` responses include a `WWW-Authenticate: Bearer …` header.
14
14
  | Code | HTTP | Meaning | Hint / next step | Channels |
15
15
  | --- | --- | --- | --- | --- |
16
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_outcome_unknown` | 409 | A past-cap deployment can still change live flows | All approvals require settlement, confirm live checked with the matching attempt, restart, or an audited override; rejection is allowed with a warning | admin |
17
18
  | `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
19
  | `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
20
  | `rebase_ack_required` | 409 | The merged result needs review (mergedHash included) | Review the diff and approve with its mergedHash | admin |
20
21
  | `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 |
22
+ | `conflict_not_found` | 409 | The requested conflict no longer exists | Reload the proposal and its conflicts | REST, MCP, admin |
23
+ | `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
24
  | `flow_id_mismatch` | 400 | Resolution tried to change the proposal flow id | Keep the same flowId or submit a new proposal | REST, MCP |
24
25
  | `access_link_missing` | 401 | No Bearer / path token | Ask the user for a new access link of the right kind | GET, REST, MCP |
25
26
  | `bearer_scheme_required` | 401 | Auth used Basic (or non-Bearer) | Send `Authorization: Bearer <token>` | REST, MCP |
@@ -49,7 +50,7 @@ GET-link `401` responses include a `WWW-Authenticate: Bearer …` header.
49
50
  | `version_mismatch` | 409 | Unknown or unavailable `rev` / `expectedRev` snapshot | Re-read flows and resubmit | GET, REST, MCP |
50
51
  | `proposal_not_found` | 404 | Unknown proposal id | List proposals and use an existing id | GET, REST, MCP |
51
52
  | `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 | Withdraw of another token’s proposal | Only the creating token can withdraw | GET, REST, MCP |
53
+ | `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 |
53
54
  | `validation_failed` | 400 | Several validation errors at once (message `N validation errors`) | Fix every entry in errors[] and resubmit. | GET, REST, MCP |
54
55
  | `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 |
55
56
  | `missing_node_id` | 400 | Node / op without an id | Include a non-empty string id | GET, REST, MCP |
package/docs/mcp-tools.md CHANGED
@@ -45,7 +45,7 @@ Get detail for one installed node type: defaults (JSON), inputs/outputs (always
45
45
 
46
46
  ## `get_proposal`
47
47
 
48
- Get any proposal by id (all tokens can read). Response includes mine:true|false and, for open proposals (pending or conflict), conflictingProposals when other open work touches the same flow. Lean by default (no payload/baseFlow); pass include_payload=true for full bodies. Cite displayName + reviewUrl when talking to the user.
48
+ Get any proposal by id (all tokens can read). Response includes mine:true|false and, for open proposals (pending or conflict), conflictingProposals when other open work touches the same flow. baseRev aliases expectedRev; baseRevChanged is informational. rebase.state=clean|merged means mergeable; conflict blocks approval. Lean by default (no payload/baseFlow); pass include_payload=true for full bodies. Cite displayName + reviewUrl when talking to the user.
49
49
 
50
50
  | Parameter | Required | Type | Description |
51
51
  | --- | --- | --- | --- |
@@ -109,14 +109,16 @@ List proposals visible to this access link (all proposals by default). Each item
109
109
 
110
110
  ## `resolve_proposal_conflict`
111
111
 
112
- Replace your own open proposal with a resolution based on expectedRev. Revision changes auto-rebase; conflict blocks approval until resolved. Open proposals expire after the configured TTL (default 7 days). Read the current flow first; payload accepts a full flow or nodes ops as in submit_proposal.
112
+ Resolve your own open proposal. Different node/tab fields auto-merge; only fields changed on both sides conflict (x/y never conflict). Choose one current conflict using nodeId + choice, preserving all non-conflicting fields. Alternatively replace the proposal using expectedRev + payload: nodes ops apply to the flow snapshot at expectedRev, NOT the stored proposal or merged result; include every intended proposal edit. Read get_proposal_diff for conflicts and get_flow for a fresh rev. These two modes cannot be mixed.
113
113
 
114
114
  | Parameter | Required | Type | Description |
115
115
  | --- | --- | --- | --- |
116
- | `expectedRev` | yes | string | — |
117
- | `id` | yes | string | — |
118
- | `payload` | yes | object | — |
119
- | `title` | no | string | — |
116
+ | `choice` | no | "live" \| "proposed" | For this conflict, keep live or proposed values of overlapping fields only. Disjoint edits survive. Use with nodeId, without expectedRev/title/payload. |
117
+ | `expectedRev` | no | string | Revision of the flow snapshot on which replacement payload ops run. Required with payload; normally the current get_flow rev. |
118
+ | `id` | yes | string | Your open proposal id (prop-…) |
119
+ | `nodeId` | no | string | Conflict nodeId from rebase.conflicts; use the flow id for tab/env conflicts. Required with choice. |
120
+ | `payload` | no | object | Replacement body; required with expectedRev, omitted with nodeId + choice |
121
+ | `title` | no | string | Optional replacement title (body replacement mode only) |
120
122
 
121
123
  ## `search_flows`
122
124
 
@@ -151,7 +153,7 @@ Submit a replace_flow proposal for owner approval (does not deploy). Preferred f
151
153
 
152
154
  ## `withdraw_proposal`
153
155
 
154
- Withdraw one of YOUR open (pending or conflict) proposals (status becomes withdrawn, not rejected). Optional reason. Only the creating access link can withdraw; other tokens get 403. Cite displayName + reviewUrl when telling the user.
156
+ Withdraw one of YOUR open (pending or conflict) proposals (status becomes withdrawn, not rejected). Optional reason. Only the creating access link can withdraw via MCP; other tokens get 403. If it is lost, a full admin session can recover via POST /proposals/{id}/withdraw. Cite displayName + reviewUrl when telling the user.
155
157
 
156
158
  | Parameter | Required | Type | Description |
157
159
  | --- | --- | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@com55/node-red-ai-access",
3
- "version": "0.2.0-beta.7",
3
+ "version": "0.2.0-beta.9",
4
4
  "description": "Node-RED plugin that gives AI agents temporary, owner-approved access via REST and MCP",
5
5
  "main": "dist/plugin.cjs",
6
6
  "types": "dist/plugin.d.cts",
@@ -24,6 +24,7 @@
24
24
  "files": [
25
25
  "dist",
26
26
  "!dist/**/*.map",
27
+ "!dist/.production-build-hash",
27
28
  "docs",
28
29
  "ai-access.cjs",
29
30
  "ai-access.html",