@com55/node-red-ai-access 0.2.0-beta.7 → 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.
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. */
@@ -303,6 +308,20 @@ interface Proposal {
303
308
  };
304
309
  /** Last auto-rebase result (pending / conflict proposals). */
305
310
  rebase?: ProposalRebaseInfo;
311
+ /** AI resolutions that replace editor changes and still require reviewer acknowledgement. */
312
+ aiConflictOverrides?: AiConflictOverride[];
313
+ /** Latest manual resolution, retained for admin undo until the next body edit. */
314
+ conflictChoice?: {
315
+ nodeId: string;
316
+ choice: 'live' | 'proposed';
317
+ chooser?: 'ai' | 'human';
318
+ undo?: {
319
+ baseFlow: Partial<FlowTab>;
320
+ flow: Partial<FlowTab>;
321
+ warnings: ProposalWarning[];
322
+ aiConflictOverrides?: AiConflictOverride[];
323
+ };
324
+ };
306
325
  /**
307
326
  * Short human title (≤80 chars). Set at submit (AI-supplied or auto-generated).
308
327
  * Optional on legacy proposals; API responses always resolve a title.
@@ -388,6 +407,8 @@ declare class ProposalStore {
388
407
  property: string;
389
408
  }[];
390
409
  warnings?: ProposalWarning[];
410
+ conflictChoice?: Proposal['conflictChoice'];
411
+ aiConflictOverrides?: AiConflictOverride[];
391
412
  }): Proposal;
392
413
  /** True while an approve holds this proposal's lock. */
393
414
  isApproving(id: string): boolean;
@@ -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
@@ -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
@@ -1054,9 +1062,13 @@ paths:
1054
1062
  (long text fields listed for expandable UI). Modified nodes include
1055
1063
  line-level diffs for text fields such as `func` and `template`.
1056
1064
 
1057
- `baseRevStale` is true only for **open** proposals when
1065
+ `baseRev` aliases `expectedRev`, the stored proposal base revision.
1066
+ `baseRevChanged` is true only for **open** proposals when
1058
1067
  `expectedRev` does not match the live revision (informational).
1059
- Closed proposals never set `baseRevStale`. Open proposals include
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
1060
1072
  `rebase` and `liveChanges` (editor changes since the stored base).
1061
1073
  Without conflicts, `vsCurrent` compares the merged deployment to live.
1062
1074
  Merges use raw stored proposals and live flows; AI diff sections are redacted.
@@ -1087,7 +1099,7 @@ paths:
1087
1099
  /proposals/{id}/resolve-conflict:
1088
1100
  post:
1089
1101
  tags: [Proposals]
1090
- summary: Choose live or proposed for one current conflict
1102
+ summary: Choose live or proposed for one current conflict, or undo the latest choice
1091
1103
  operationId: resolveProposalConflict
1092
1104
  security:
1093
1105
  - AdminSession: []
@@ -1145,7 +1157,12 @@ paths:
1145
1157
  /proposals/{id}/resolve:
1146
1158
  post:
1147
1159
  tags: [Proposals]
1148
- summary: Resolve your own open proposal with a fresh payload
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.
1149
1166
  operationId: resolveProposal
1150
1167
  security:
1151
1168
  - AccessLinkBearer: []
@@ -1272,14 +1289,16 @@ paths:
1272
1289
  /proposals/{id}/withdraw:
1273
1290
  post:
1274
1291
  tags: [Proposals]
1275
- summary: Withdraw own pending proposal
1292
+ summary: Withdraw an open proposal (owner token or full admin recovery)
1276
1293
  description: |
1277
- Access-link Bearer only. Withdraws an **open** (pending or conflict) proposal created by
1278
- 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
1279
1297
  JSON body `{ "reason": "…" }`.
1280
1298
  operationId: withdrawProposal
1281
1299
  security:
1282
1300
  - AccessLinkBearer: []
1301
+ - AdminSession: []
1283
1302
  parameters:
1284
1303
  - $ref: '#/components/parameters/ProposalId'
1285
1304
  requestBody:
@@ -1301,7 +1320,7 @@ paths:
1301
1320
  '401':
1302
1321
  $ref: '#/components/responses/Unauthorized'
1303
1322
  '403':
1304
- description: Not owned by this access link
1323
+ description: Not owned by this access link or admin lacks full permission
1305
1324
  content:
1306
1325
  application/json:
1307
1326
  schema:
@@ -1585,7 +1604,7 @@ components:
1585
1604
  - `version_mismatch` (409): Unknown or unavailable `rev` / `expectedRev` snapshot
1586
1605
  - `proposal_not_found` (404): Unknown proposal id
1587
1606
  - `proposal_not_pending` (409): Action needs an open proposal
1588
- - `proposal_not_owned` (403): Withdraw of another token’s proposal
1607
+ - `proposal_not_owned` (403): Resolve or withdraw another token’s proposal
1589
1608
  - `validation_failed` (400): Several validation errors at once (message `N validation errors`)
1590
1609
  - `duplicate_node_id` (400): The same node id appears more than once in a full tab or batch
1591
1610
  - `missing_node_id` (400): Node / op without an id
@@ -2346,6 +2365,7 @@ components:
2346
2365
  - type
2347
2366
  - status
2348
2367
  - expectedRev
2368
+ - baseRev
2349
2369
  - payload
2350
2370
  - createdBy
2351
2371
  - createdAt
@@ -2355,6 +2375,16 @@ components:
2355
2375
  - reviewUrl
2356
2376
  - reviewPath
2357
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]
2358
2388
  deployPending:
2359
2389
  type: object
2360
2390
  description: Timed-out deployment has an unknown outcome; approvals wait until settlement or the hard cap. Check live flows before approving again.
@@ -2378,6 +2408,12 @@ components:
2378
2408
  $ref: '#/components/schemas/ProposalStatus'
2379
2409
  expectedRev:
2380
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.
2381
2417
  payload:
2382
2418
  $ref: '#/components/schemas/ReplaceFlowPayload'
2383
2419
  baseFlow:
@@ -2536,7 +2572,7 @@ components:
2536
2572
  type: object
2537
2573
  required: [kind]
2538
2574
  description: |
2539
- Admin for approve/reject/fail; access-link for withdraw.
2575
+ Admin for approve/reject/fail and recovery withdrawal; access-link for owner withdrawal.
2540
2576
  properties:
2541
2577
  kind:
2542
2578
  type: string
@@ -2596,24 +2632,44 @@ components:
2596
2632
  type: string
2597
2633
  choice:
2598
2634
  type: string
2599
- enum: [live, proposed]
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.
2600
2637
 
2601
2638
  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
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]
2617
2673
 
2618
2674
  ApproveProposalRequest:
2619
2675
  type: object
@@ -2730,6 +2786,11 @@ components:
2730
2786
  type: object
2731
2787
  required: [summary, added, removed, modified]
2732
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'
2733
2794
  summary:
2734
2795
  type: object
2735
2796
  required: [added, removed, modified, tabChanges]
@@ -2766,7 +2827,8 @@ components:
2766
2827
  - flowId
2767
2828
  - expectedRev
2768
2829
  - currentRev
2769
- - baseRevStale
2830
+ - baseRevChanged
2831
+ - baseRev
2770
2832
  - compareAgainst
2771
2833
  properties:
2772
2834
  proposalId:
@@ -2775,9 +2837,12 @@ components:
2775
2837
  type: string
2776
2838
  expectedRev:
2777
2839
  type: string
2840
+ baseRev:
2841
+ type: string
2842
+ description: Alias of expectedRev, the stored base revision.
2778
2843
  currentRev:
2779
2844
  type: string
2780
- baseRevStale:
2845
+ baseRevChanged:
2781
2846
  type: boolean
2782
2847
  description: |
2783
2848
  Informational: true for open proposals when expectedRev ≠ live rev.
@@ -2790,6 +2855,26 @@ components:
2790
2855
  description: True for legacy proposals without a stored baseFlow
2791
2856
  rebase:
2792
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: {}
2793
2878
  liveChanges:
2794
2879
  $ref: '#/components/schemas/ProposalDiffSections'
2795
2880
  vsCurrent:
package/docs/errors.md CHANGED
@@ -18,8 +18,8 @@ GET-link `401` responses include a `WWW-Authenticate: Bearer …` header.
18
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
19
  | `rebase_ack_required` | 409 | The merged result needs review (mergedHash included) | Review the diff and approve with its mergedHash | admin |
20
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 |
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
23
  | `flow_id_mismatch` | 400 | Resolution tried to change the proposal flow id | Keep the same flowId or submit a new proposal | REST, MCP |
24
24
  | `access_link_missing` | 401 | No Bearer / path token | Ask the user for a new access link of the right kind | GET, REST, MCP |
25
25
  | `bearer_scheme_required` | 401 | Auth used Basic (or non-Bearer) | Send `Authorization: Bearer <token>` | REST, MCP |
@@ -49,7 +49,7 @@ GET-link `401` responses include a `WWW-Authenticate: Bearer …` header.
49
49
  | `version_mismatch` | 409 | Unknown or unavailable `rev` / `expectedRev` snapshot | Re-read flows and resubmit | GET, REST, MCP |
50
50
  | `proposal_not_found` | 404 | Unknown proposal id | List proposals and use an existing id | GET, REST, MCP |
51
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 | Withdraw of another token’s proposal | Only the creating token can withdraw | 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 |
53
53
  | `validation_failed` | 400 | Several validation errors at once (message `N validation errors`) | Fix every entry in errors[] and resubmit. | GET, REST, MCP |
54
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 |
55
55
  | `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.8",
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",