@xano/cli 1.3.0 → 1.3.1-beta.0

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.
Files changed (96) hide show
  1. package/README.md +269 -1
  2. package/dist/base-command.d.ts +12 -1
  3. package/dist/base-command.js +36 -22
  4. package/dist/commands/branch/set_live/index.d.ts +22 -0
  5. package/dist/commands/branch/set_live/index.js +60 -1
  6. package/dist/commands/ephemeral/push/index.js +6 -1
  7. package/dist/commands/function/create/index.d.ts +15 -0
  8. package/dist/commands/function/create/index.js +40 -0
  9. package/dist/commands/function/edit/index.d.ts +15 -0
  10. package/dist/commands/function/edit/index.js +40 -0
  11. package/dist/commands/policy/catalogue/index.d.ts +15 -0
  12. package/dist/commands/policy/catalogue/index.js +29 -0
  13. package/dist/commands/policy/create/index.d.ts +18 -0
  14. package/dist/commands/policy/create/index.js +39 -0
  15. package/dist/commands/policy/delete/index.d.ts +19 -0
  16. package/dist/commands/policy/delete/index.js +61 -0
  17. package/dist/commands/policy/evaluate/index.d.ts +32 -0
  18. package/dist/commands/policy/evaluate/index.js +71 -0
  19. package/dist/commands/policy/list/index.d.ts +14 -0
  20. package/dist/commands/policy/list/index.js +21 -0
  21. package/dist/commands/policy/parse/index.d.ts +19 -0
  22. package/dist/commands/policy/parse/index.js +18 -0
  23. package/dist/commands/policy/publish/index.d.ts +20 -0
  24. package/dist/commands/policy/publish/index.js +38 -0
  25. package/dist/commands/policy/runs/index.d.ts +54 -0
  26. package/dist/commands/policy/runs/index.js +273 -0
  27. package/dist/commands/policy/status/index.d.ts +21 -0
  28. package/dist/commands/policy/status/index.js +54 -0
  29. package/dist/commands/release/create/index.js +7 -0
  30. package/dist/commands/release/deploy/index.d.ts +28 -0
  31. package/dist/commands/release/deploy/index.js +90 -6
  32. package/dist/commands/release/import/index.js +5 -1
  33. package/dist/commands/release/list/index.d.ts +7 -0
  34. package/dist/commands/release/list/index.js +54 -10
  35. package/dist/commands/release/push/index.js +19 -1
  36. package/dist/commands/sandbox/push/index.js +6 -1
  37. package/dist/commands/skills/pull/index.d.ts +30 -0
  38. package/dist/commands/skills/pull/index.js +118 -0
  39. package/dist/commands/tenant/create/index.d.ts +3 -0
  40. package/dist/commands/tenant/create/index.js +21 -0
  41. package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
  42. package/dist/commands/tenant/deploy_release/index.js +186 -4
  43. package/dist/commands/tenant/edit/index.d.ts +2 -0
  44. package/dist/commands/tenant/edit/index.js +39 -4
  45. package/dist/commands/tenant_deploy_request/bypass/index.d.ts +31 -0
  46. package/dist/commands/tenant_deploy_request/bypass/index.js +109 -0
  47. package/dist/commands/tenant_deploy_request/create/index.d.ts +21 -0
  48. package/dist/commands/tenant_deploy_request/create/index.js +108 -0
  49. package/dist/commands/tenant_deploy_request/edit/index.d.ts +22 -0
  50. package/dist/commands/tenant_deploy_request/edit/index.js +103 -0
  51. package/dist/commands/tenant_deploy_request/get/index.d.ts +25 -0
  52. package/dist/commands/tenant_deploy_request/get/index.js +116 -0
  53. package/dist/commands/tenant_deploy_request/list/index.d.ts +20 -0
  54. package/dist/commands/tenant_deploy_request/list/index.js +122 -0
  55. package/dist/commands/tenant_deploy_request/revision/index.d.ts +21 -0
  56. package/dist/commands/tenant_deploy_request/revision/index.js +83 -0
  57. package/dist/commands/tenant_deploy_request/set_status/index.d.ts +38 -0
  58. package/dist/commands/tenant_deploy_request/set_status/index.js +159 -0
  59. package/dist/commands/workspace/pull/index.d.ts +10 -0
  60. package/dist/commands/workspace/pull/index.js +70 -7
  61. package/dist/commands/workspace/push/index.d.ts +19 -0
  62. package/dist/commands/workspace/push/index.js +124 -5
  63. package/dist/policy-command.d.ts +90 -0
  64. package/dist/policy-command.js +94 -0
  65. package/dist/utils/document-parser.d.ts +10 -0
  66. package/dist/utils/document-parser.js +21 -0
  67. package/dist/utils/multidoc-push.d.ts +55 -2
  68. package/dist/utils/multidoc-push.js +103 -27
  69. package/dist/utils/policy/catalogue.d.ts +7 -0
  70. package/dist/utils/policy/catalogue.js +72 -0
  71. package/dist/utils/policy/errors.d.ts +12 -0
  72. package/dist/utils/policy/errors.js +61 -0
  73. package/dist/utils/policy/feedback.d.ts +68 -0
  74. package/dist/utils/policy/feedback.js +165 -0
  75. package/dist/utils/policy/findings.d.ts +23 -0
  76. package/dist/utils/policy/findings.js +65 -0
  77. package/dist/utils/policy/gate.d.ts +161 -0
  78. package/dist/utils/policy/gate.js +201 -0
  79. package/dist/utils/policy/goal.d.ts +46 -0
  80. package/dist/utils/policy/goal.js +95 -0
  81. package/dist/utils/policy/permission.d.ts +23 -0
  82. package/dist/utils/policy/permission.js +84 -0
  83. package/dist/utils/policy/push-gate.d.ts +5 -0
  84. package/dist/utils/policy/push-gate.js +29 -0
  85. package/dist/utils/policy/release.d.ts +85 -0
  86. package/dist/utils/policy/release.js +115 -0
  87. package/dist/utils/policy/request.d.ts +55 -0
  88. package/dist/utils/policy/request.js +115 -0
  89. package/dist/utils/policy/runs.d.ts +30 -0
  90. package/dist/utils/policy/runs.js +101 -0
  91. package/dist/utils/policy/status.d.ts +47 -0
  92. package/dist/utils/policy/status.js +117 -0
  93. package/dist/utils/policy/types.d.ts +185 -0
  94. package/dist/utils/policy/types.js +1 -0
  95. package/oclif.manifest.json +6132 -3133
  96. package/package.json +7 -1
package/README.md CHANGED
@@ -170,6 +170,104 @@ xano profile delete myprofile
170
170
  xano profile delete myprofile --force
171
171
  ```
172
172
 
173
+ ### Policies
174
+
175
+ Policies combine a description with deterministic check rules; the platform parses, validates and evaluates
176
+ them. `xano policy <command> --help` gives each command's flags and output, and the Xano developer MCP documents
177
+ the rest: the `policies` topic of `xano_xanoscript_docs` (the language) and the `policy` topic of `xano_cli_docs`.
178
+
179
+ ```bash
180
+ xano policy catalogue --check object.auth_required # The checks a rule can use (all of them without --check)
181
+ xano policy create --goal endpoints_need_login -b dev # Create an independent policy from a platform goal
182
+ xano policy create --goal no_pii_in_responses --key DATA-020 --param 'fields=["email","ssn"]'
183
+ xano policy list # Policies on the branch
184
+ xano policy parse policies/AUTH-001.xs # Validate and print canonical XanoScript (or --file, --stdin)
185
+ xano policy publish policies/AUTH-001.xs -m "Why" # Create or update by key; -m labels the Version History entry
186
+ xano policy evaluate # Evaluate the branch now
187
+ xano policy evaluate --summary # The same, answering the run summary and its first 50 findings
188
+ xano policy status --fail-on-findings # The latest run, without evaluating (CI)
189
+ xano policy runs 1674 --run-detail # One run and its first 100 findings; without an id, the retained runs
190
+ xano policy runs 1674 --offset 100 --limit 100 # The next page of that run's findings (at most 500 a page)
191
+ xano policy runs 1674 --all -o json # Every finding, read page by page
192
+ xano policy runs 1674 --blocking --policy AUTH-001 # Only some findings: --blocking/--advisory, --policy, --rule,
193
+ # --severity, --kind, --object type:id, --tag (repeatable), --search
194
+ xano policy runs --release v1.2 # A release's check (stored when it was cut), paged and filtered the same way
195
+ xano policy runs --release v1.2 --recheck # Check the release again from its archive, store the run, then read it
196
+ xano policy runs --release-id 12 # Read an imported release by ID without a name lookup
197
+ xano policy delete TMP-001 # Remove a policy; its Version History is kept
198
+ ```
199
+
200
+ `policy create --goal <id>` reads the instance's goals from `policy catalogue -o json`, copies a goal's
201
+ sparse settings, validates them natively and creates an Active, Advisory policy. Required goal settings must
202
+ be filled before it saves. Repeat `--param 'N.path=JSON'` for overrides: rule numbers start at 1; the `N.`
203
+ prefix is optional for a single-rule goal. Nested paths work, for example `--param '1.when.statement="db.edit"'`.
204
+ Values are JSON, including quoted strings, arrays, booleans and numbers. The goal key gets a free numeric
205
+ suffix if already taken (case-insensitive); an explicitly taken `--key` is refused. It never updates an
206
+ existing policy. `-m/--message` labels its Version History entry; `-o json` returns the saved policy and
207
+ native rule warnings go to stderr. No continuing link to the goal is stored.
208
+
209
+ A push to the live branch is refused before commit when mandatory policies block it. The gate judges the branch by the policies it will have after the push, so the pushed policy files count, and a push that weakens a mandatory policy of the live branch (demotes, deactivates or deletes it, or loosens one of its rules) needs `workspace:policy` update and is otherwise refused before anything is imported. A 403 `policy_gate` exits **2**, says nothing was imported, and lists the blocking findings and introduced/changed counts. With `-o json` it prints `{imported: false, refused: <payload>}`. `--policy-override "reason"` sends an audited override, requiring `workspace:policy` update permission. This gate requires transactions: remove `--no-transaction` if the server refuses it (exit 1).
210
+
211
+ Every policy command takes `-w/--workspace`, `-b/--branch` (the profile's branch by default; `-b ''` is live)
212
+ and `-o/--output summary|json`. `workspace pull` and `workspace push` carry policies as `policies/<KEY>.xs`.
213
+ Policies use `active = true|false` before `enforcement`; authored `lifecycle` is refused. The list shows
214
+ `Active, Mandatory`, `Active, Advisory` or `Inactive`. Inactive policies are saved directly and skipped by
215
+ branch checks; `policy status` reports `inactive`. An inactive policy may have no rules.
216
+ `release push` carries them too, for a credential holding `workspace:policy` create and update: a release ships its
217
+ policies like its code, and a tenant deploy lands them (see Releases). Tier1 tenant, sandbox and ephemeral pushes
218
+ carry them as well; a remote tenant push, or a tenant created before policies, leaves them out and prints the
219
+ platform's notice.
220
+ While the instance's Policies feature is off, `workspace pull` exports no policy (and keeps local policy files)
221
+ and `workspace push` leaves its policy files out, printing the platform's notice instead of a policy count.
222
+
223
+ A run can hold tens of thousands of findings, so the CLI reads them a page at a time. `policy runs <id>` reads
224
+ the run's summary and one page of its findings; a page that is not every finding says `(one page)` and how to read
225
+ the next. `-o json` is the summary with `findings`, the page's findings as an array, and
226
+ `findings_page: {total, offset, limit, next_offset}`. `--all` reads every page (500 a request, saying so on stderr
227
+ when there are more) and returns every matching finding in `findings`. `policy status` reads the latest run's
228
+ summary, so its `-o json` `run` carries each policy's finding count rather than the findings. The `policy_check` a
229
+ `workspace push` prints lists the first 100 findings; when there are more it prints their counts and
230
+ `Listed: the first 100 of N; \`xano policy runs <run_id>\` has them all.` `policy evaluate --summary` does the same
231
+ with the first 50, and `policy evaluate` without it still answers the whole run.
232
+
233
+ `policy evaluate --policy AUTH-001` tries that one policy alone, even if inactive. It never stores or audits
234
+ the trial, replaces the latest run or gates anything. The output names each rule's clear objects (up to five
235
+ names, with the rest counted). Combine it with `--summary`, `--run-detail` or `-o json`; an inactive trial
236
+ does not exit 2. A blank key is refused before any request, and an unknown key gets the platform's 404.
237
+
238
+ `policy runs --release <name>` reads a release's check: the run stored when the release was cut, evaluated on what
239
+ the release ships against the policies it ships. It takes every finding flag a run read takes, `-o json` has the same
240
+ shape (plus the release's `shipped` policies), and a release with no check says so (`{"run": null}` under `-o json`).
241
+ `--recheck` checks the release again from its archive and stores the new run, so a read-only credential is refused.
242
+ `--release-id <id>` reads the same report directly by ID and also supports `--recheck`; it is exclusive with
243
+ `--release` and `--branch`.
244
+
245
+ **Exit codes** of `policy evaluate`, `workspace push` and `policy status --fail-on-findings`: `2` when a finding
246
+ blocks (an active, mandatory policy failed), whatever else happened; `1` when a request or the import failed, or
247
+ when `status --fail-on-findings` finds stale, missing or errored evidence; otherwise `0`. `tenant deploy_release`,
248
+ `tenant_deploy_request set_status` and `tenant_deploy_request bypass` exit `2` when the tenant deploy policy gate
249
+ refuses the deploy (and `tenant deploy_release --check` when it would), `1` for any other failure, otherwise `0`.
250
+ `release deploy --set_live` and `branch set_live` exit `2` when the set-live policy gate refuses to set the branch live, and `1` for any other failure; `branch set_live --policy-override "<reason>"` sets it live past the gate with an audited reason, as `release deploy` does. `function create` and `function edit` exit `2` when the live-branch publish gate refuses the save (see Functions), and `1` for any other failure. A blank `--policy-override` is refused before any request. A `workspace push`, `branch set_live`, `release deploy --set_live` or `policy delete` refused with `policy_weakening_permission_required` exits `1`, like the other permission refusals: it is a missing permission, not a blocking finding.
251
+
252
+ Each route needs the `workspace:policy` permission at the request's level. A refusal's `payload.code` names the
253
+ gate and the CLI prints its remedy: `policy_feature_disabled` (Policies are off on the instance),
254
+ `policy_permission_required` (an instance admin grants your role the level) or `policy_scope_required` (create a
255
+ Metadata API token that has the scope). A push to the live branch, a set live or a merge is judged by the policies it
256
+ leaves, so one that weakens a policy active and mandatory on the branch (demotes, deactivates or deletes it, or
257
+ loosens one of its rules; for a set live, a live mandatory policy the branch lacks counts as deleted) needs
258
+ `workspace:policy` update, and without it is refused before anything is written with
259
+ `policy_weakening_permission_required`, naming the policies. `--policy-override` does not lift it: the CLI prints the
260
+ platform's sentence and how to go on (a push can leave the policy files out with `-e "policies/*"`), and a push's
261
+ `--dry-run` is refused the same way. `policy delete` of an active, mandatory policy needs update too, not only delete,
262
+ and is refused with the same code otherwise. A set live whose check could not read the policies fails closed: only
263
+ update proceeds, and the refusal says so (`unavailable`). A rule naming a check the instance does not have (`policy_unknown_check`)
264
+ points at `xano policy catalogue`. `policy delete` sends the `updated_at` it listed, so a policy changed since is
265
+ refused (`policy_stale`) rather than deleted; it asks first, and without `--force` in a non-interactive shell it
266
+ exits 1. A create past the plan's per-branch policy cap (lower plans cap the policies on each branch; Pro and above are unlimited) is refused with
267
+ `policy_plan_limit`, naming the plan, and a `workspace push` whose new policies would not fit imports nothing.
268
+ `policy parse` and `policy publish` print rule warnings (a part of a rule that would do nothing or allow too much on
269
+ that branch) on stderr; `-o json` keeps stdout the JSON.
270
+
173
271
  ### Workspaces
174
272
 
175
273
  ```bash
@@ -197,6 +295,7 @@ xano workspace delete -w <workspace_id> --force
197
295
  xano workspace pull
198
296
  xano workspace pull -d ./my-workspace # Specify output directory
199
297
  xano workspace pull -b dev # Specific branch
298
+ xano workspace pull -b '' # The live branch, whatever the profile's branch
200
299
  xano workspace pull --env --records # Include env vars and table records
201
300
  xano workspace pull --draft # Include draft changes
202
301
 
@@ -204,6 +303,7 @@ xano workspace pull --draft # Include draft changes
204
303
  xano workspace push
205
304
  xano workspace push -d ./my-workspace # Push from a specific directory
206
305
  xano workspace push -b dev
306
+ xano workspace push -b '' # The live branch, whatever the profile's branch
207
307
  xano workspace push --sync # Full push — send all files, not just changed ones
208
308
  xano workspace push --sync --delete # Full push + delete remote objects not included
209
309
  xano workspace push --dry-run # Preview changes without pushing
@@ -216,6 +316,9 @@ xano workspace push --force # Skip preview and conf
216
316
  xano workspace push -i "function/*" # Push only matching files
217
317
  xano workspace push -e "table/*" # Push all files except tables
218
318
  xano workspace push -i "function/*" -e "**/test*" # Include functions, exclude tests
319
+ xano workspace push -o json # One JSON document: the preview (with --dry-run) or the import result with policy_check
320
+ xano workspace push -m "Tightened the auth policies" # Label the Version History entry of each policy this push changes; other objects get no message
321
+ xano workspace push --policy-override "Approved exception" # Proceed past a blocking live-branch gate with an audited reason
219
322
 
220
323
  # Pull from a git repository to local files (defaults to current directory)
221
324
  xano workspace git pull -r https://github.com/owner/repo
@@ -330,6 +433,7 @@ xano branch edit <branch_label> --color "#ff0000"
330
433
  # Set live branch
331
434
  xano branch set_live <branch_label>
332
435
  xano branch set_live <branch_label> --force
436
+ xano branch set_live <branch_label> --policy-override "Rollback approved" # Past a blocking set-live policy gate (audited)
333
437
 
334
438
  # Delete a branch
335
439
  xano branch delete <branch_label>
@@ -354,6 +458,7 @@ xano function get <function_id> --include_draft # Include draft version
354
458
  xano function create -f function.xs
355
459
  xano function create -f function.xs --edit # Open in $EDITOR before creating
356
460
  cat function.xs | xano function create --stdin
461
+ xano function create -f function.xs --policy-override "Exception approved" # Past a blocking publish policy gate (audited)
357
462
 
358
463
  # Edit a function
359
464
  xano function edit <function_id> # Opens in $EDITOR
@@ -361,6 +466,7 @@ xano function edit <function_id> -f new.xs # Update from file
361
466
  xano function edit <function_id> -f new.xs --edit # Open in $EDITOR before updating
362
467
  cat function.xs | xano function edit <function_id> --stdin # Update from stdin
363
468
  xano function edit <function_id> --no-publish # Edit without publishing
469
+ xano function edit <function_id> -f new.xs --policy-override "Exception approved" # Past a blocking publish policy gate (audited)
364
470
 
365
471
  # Run (execute) a function by name
366
472
  xano function run <name> # Prompts for declared inputs (on a TTY)
@@ -371,6 +477,11 @@ xano function run <name> --branch dev --logs # run on a branch, show
371
477
  xano function run <name> --datasource test # run against the 'test' data source
372
478
  ```
373
479
 
480
+ `function create` and `function edit` save a function on the live branch through the publish policy gate. When
481
+ blocking findings refuse the save, nothing is saved: the command prints the verdict and the platform's message and
482
+ exits `2` (any other failure exits `1`), and `-o json` prints `{created|updated: false, message, policy_gate}`.
483
+ `--policy-override "<reason>"` saves past the gate with an audited reason (requires `workspace:policy` update).
484
+
374
485
  Input flexibility for `function run` (assembled into one JSON `input` object):
375
486
 
376
487
  | Form | Meaning |
@@ -421,12 +532,27 @@ xano knowledge get "deploy-runbook" -w 40 --output json
421
532
  xano knowledge get "deploy-runbook" -w 40 --file checklist.md
422
533
  ```
423
534
 
535
+ ### Agent skills
536
+
537
+ ```bash
538
+ xano skills pull # Writes ./.claude/skills/xano-policies/SKILL.md
539
+ xano skills pull -d ./my-project # Install into another project directory
540
+ xano skills pull -b dev -o json # Another branch; print the result as JSON
541
+ ```
542
+
543
+ `xano skills pull` writes the `xano-policies` skill the instance generates from its check catalogue to
544
+ `<directory>/.claude/skills/xano-policies/SKILL.md`, replacing that file without a backup; pull it again after an
545
+ instance upgrade. A copy installed anywhere else is not touched, so remove it. The route needs the
546
+ `workspace:policy` read permission. The CLI writes the file's frontmatter (`name` and `description`) itself and drops
547
+ any frontmatter the served content opens with, so a workspace knowledge record that replaces the platform skill cannot
548
+ set agent settings such as `allowed-tools` or `hooks`. It refuses to write through a symbolic link at that path.
549
+
424
550
  ### Releases
425
551
 
426
552
  All release commands use **release names** (e.g., `v1.0`), not IDs.
427
553
 
428
554
  ```bash
429
- # List releases
555
+ # List releases, each tagged with its stored policy check when Policies is enabled and readable (see Policies)
430
556
  xano release list
431
557
 
432
558
  # Get release details
@@ -467,8 +593,54 @@ xano release deploy "v1.0"
467
593
  xano release deploy "v1.0" --force
468
594
  xano release deploy "v1.0" --branch "restore-v1" --no-set_live
469
595
  xano release deploy "v1.0" -w 40 -o json --force
596
+
597
+ # Set it live past a blocking set-live policy gate with an audited reason (needs workspace:policy update)
598
+ xano release deploy "v1.0" --branch "rollback-v1" --set_live --policy-override "Rollback approved; tracked in JIRA-12"
599
+
600
+ # Read a release's policy check, or check it again (see Policies)
601
+ xano policy runs --release v1.0
602
+ xano policy runs --release v1.0 --recheck
470
603
  ```
471
604
 
605
+ A release carries its branch's policies like its code (`release push` sends `policies/*.xs` with the rest), and a
606
+ tenant deploy lands them on the tenant. A release built by `release push` carries those files only for a credential
607
+ holding `workspace:policy` create and update; for anyone else it carries the workspace's live-branch policies instead,
608
+ and when the files differ from them the push prints the platform's notice naming them (exit `0`). `release import` of
609
+ an archive that carries policies needs the same levels: without them nothing is stored, and the command fails with
610
+ the platform's message and the remedy for your role or token.
611
+
612
+ Cutting a release (`release create`, `release push`, `release import`) stores its policy check, and the summary
613
+ prints it: `Policy check: fail (run 1712), 2 blocking, 1 advisory findings`, or
614
+ `Policy check disabled|skipped|error: <why>` when none was recorded. `-o json` keeps it as `policy_run`
615
+ (`{status, run_id, run_status, blocking, advisory, message?}`, or `null` for a credential without `workspace:policy`
616
+ read). A cut is never refused for its findings: its blocking findings are what gates each tenant deploy (see Tenant
617
+ deployments).
618
+
619
+ `release list` tags each release with its stored check, worded as Studio's Policies column: `[policies: 2 blocking,
620
+ 1 advisory]`, `[policies: 11 advisory]`, `[policies: passed]`, `[policies: none shipped]` (the release carries no
621
+ policies), `[policies: could not check]` (a rule could not run) or `[policies: not checked]` (no stored check:
622
+ deploying the release checks it, and so does `policy runs --release <name> --recheck`), and ends with
623
+ `Policy check details: xano policy runs --release <name>`. The tag is the release's own check, its code against the
624
+ policies it ships, and it is also what the tenant deploy gate judges: a release with blocking findings is held on
625
+ every standard or run tenant (`tenant deploy_release --check`). The list reads `release/policy_check` once, stores nothing, and
626
+ prints as it did before when that read is refused (Policies off, no `workspace:policy` read) or fails. `-o json`
627
+ adds `policy_check` to each item (`{run_id, status, finished_at, counts {findings, blocking, advisory, errors},
628
+ policies}`, or `null` for a release with no stored check) and leaves it out when the checks could not be read.
629
+ Tier1 tenant, ephemeral and sandbox pushes carry policy files too when Policies is enabled and the tenant has
630
+ a policy table. Remote tenant pushes still leave them out; remote release deploys carry them.
631
+
632
+ `release deploy --set_live` lands the release as a new branch, then sets it live through the same policy gate as
633
+ `branch set_live`: the branch is checked against its own policies, which live will have once it is set live, and
634
+ blocking findings it introduces compared with the live branch, or leaves on objects it changes, refuse it. A branch
635
+ that weakens a mandatory policy of the live branch needs `workspace:policy` update (a release that lands no policy is
636
+ judged by the live branch's policies instead, and nothing is checked for weakening): without it set live is refused
637
+ with `policy_weakening_permission_required`, the branch stays, and the command exits `1`, naming the branch to set
638
+ live or delete. A gate refusal keeps the branch, prints the verdict, says `The branch was
639
+ created; set live was refused` and exits `2`; `-o json` prints `{"branch_created": true, "set_live": false, "branch",
640
+ "message", "policy_gate"}`. Set it live from Studio's Branches panel with a reason, or delete the branch and deploy
641
+ again with `--policy-override "<reason>"`, which sets it live past the gate and is audited. A policy of the release
642
+ that the new branch could not take (its key is held by another policy) is named in a warning.
643
+
472
644
  ### Platforms
473
645
 
474
646
  ```bash
@@ -606,9 +778,14 @@ xano tenant get <tenant_name>
606
778
  xano tenant create "My Tenant"
607
779
  xano tenant create "My Tenant" -d "Description" --type tier2 --cluster_id 1 --platform_id 5
608
780
  xano tenant create "My Tenant" --type tier2 --cluster_id 1 --license ./license.yaml
781
+ xano tenant create "Production" --required_reviewers 1 --allow_deploy_bypass
782
+ xano tenant create "Staging" --required_reviewers 0 --allow_quick_deploy
609
783
 
610
784
  # Edit a tenant
611
785
  xano tenant edit <tenant_name> --display "New Name" -d "New description"
786
+ xano tenant edit <tenant_name> --required_reviewers 1
787
+ xano tenant edit <tenant_name> --required_reviewers 0
788
+ xano tenant edit <tenant_name> --allow_quick_deploy
612
789
 
613
790
  # Delete a tenant (confirmation required)
614
791
  xano tenant delete <tenant_name>
@@ -655,10 +832,101 @@ xano tenant deploy_platform <tenant_name> --platform_id 5
655
832
  # Deploy a release by name
656
833
  xano tenant deploy_release <tenant_name> --release v1.0
657
834
 
835
+ # Preview the release's policy gate on the tenant without deploying (exit 2 when it would refuse)
836
+ xano tenant deploy_release <tenant_name> --release v1.0 --check
837
+ xano tenant deploy_release <tenant_name> --release v1.0 --check -o json
838
+
839
+ # Deploy past a blocking policy gate with an audited reason (needs workspace:policy update)
840
+ xano tenant deploy_release <tenant_name> --release v1.0 --override-reason "Accepted; fix tracked in JIRA-12"
841
+
658
842
  # Deploy with a license override file (deploy_platform only)
659
843
  xano tenant deploy_platform <tenant_name> --platform_id 5 --license ./license.yaml
660
844
  ```
661
845
 
846
+ If a tenant has `required_reviewers` set above 0, `tenant deploy_release` fails with an
847
+ error naming the `tenant_deploy_request` command to run instead — see below. A tenant
848
+ with `allow_quick_deploy` enabled skips the gate entirely, regardless of
849
+ `required_reviewers`.
850
+
851
+ A release deploy is also gated on policy. The gate judges the release alone, by its own policy check (the one
852
+ stored when it was cut, or checked from its archive on demand): the blocking findings of its active mandatory policies
853
+ refuse the deploy, including findings the tenant's current release already has. It does not compare the release with
854
+ what the tenant runs, so the same release gets the same verdict on every tenant. Ephemeral and sandbox tenants get the
855
+ verdict but are never refused. A release cut before releases carried policies reads `not_carried` and is not gated.
856
+ `--check` prints the verdict (status, the release and its policy run, the number of blocking findings, and the first
857
+ of them) without deploying; `-o json` is the gate's answer as served. A refused deploy prints the same verdict and exits `2`, naming the
858
+ `--override-reason` rerun when your credential may override; under `-o json` it prints
859
+ `{"deployed": false, "message", "policy_gate"}`. `--override-reason` sends `override_policy: true` with the reason,
860
+ and the platform audits it. A successful deploy's summary prints the gate's status when the platform returns one,
861
+ and a warning names any policy of the release the tenant could not take (its key is held by another policy).
862
+
863
+ #### Tenant Deploy Requests
864
+
865
+ When a tenant has `required_reviewers` set above 0 and `allow_quick_deploy` is off (see
866
+ `tenant create`/`tenant edit` above), a release may only be deployed to it through an
867
+ **approved deploy request**, requiring that many distinct reviewer approvals. An author
868
+ with no deploy permission opens a request; a named reviewer who does hold deploy
869
+ permission on the tenant votes to approve it, and once enough reviewers have approved,
870
+ it deploys as part of that action. `allow_deploy_bypass` (set at `tenant create` time
871
+ only) additionally allows an authorized user to skip a specific request's gate via
872
+ `tenant_deploy_request bypass`, audited with a required reason.
873
+
874
+ ```bash
875
+ # List deploy requests in a workspace (results show the target tenant and release)
876
+ xano tenant_deploy_request list
877
+ xano tenant_deploy_request list --tenant prod --status pending
878
+ xano tenant_deploy_request list --tenant prod --release v1.2
879
+ xano tenant_deploy_request list --to-review
880
+
881
+ # Get details of a specific deploy request, with its release's policy check on its tenant
882
+ xano tenant_deploy_request get <id>
883
+
884
+ # Open a deploy request (submits for review immediately unless --draft is passed)
885
+ xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --reviewers 12,45
886
+ xano tenant_deploy_request create "Deploy v1.2 to prod" --tenant prod --release v1.2 --draft
887
+
888
+ # Edit a deploy request's title, description or reviewers
889
+ xano tenant_deploy_request edit <id> --title "Deploy v1.2.1 to prod"
890
+ xano tenant_deploy_request edit <id> --reviewers 12,45,67
891
+
892
+ # Change status: draft, submit, approve, request_changes, close, or reopen
893
+ # (approve records one reviewer's vote; once enough votes are in it deploys the
894
+ # release as part of that same call)
895
+ xano tenant_deploy_request set_status <id> --status submit
896
+ xano tenant_deploy_request set_status <id> --status approve
897
+ xano tenant_deploy_request set_status <id> --status approve --override-policy --reason "Accepted; fix tracked in JIRA-12"
898
+ xano tenant_deploy_request set_status <id> --status request_changes --reason "Needs a migration note"
899
+ xano tenant_deploy_request set_status <id> --status close --reason "Superseded by a newer request"
900
+ xano tenant_deploy_request set_status <id> --status reopen
901
+
902
+ # Link a newer release to a request after changes were requested (author only)
903
+ xano tenant_deploy_request revision <id> --release v1.2.1 --note "Added the missing migration"
904
+
905
+ # Deploy past the approval gate under the deploy-bypass permission (requires the
906
+ # tenant's allow_deploy_bypass to be enabled, and a separately-grantable RBAC
907
+ # permission). Every use is audited.
908
+ xano tenant_deploy_request bypass <id> --reason "Prod incident, reviewer unavailable"
909
+ xano tenant_deploy_request bypass <id> --reason "Prod incident, reviewer unavailable" --override-policy
910
+ ```
911
+
912
+ `tenant_deploy_request get` always names the tenant and release. For requests that are neither approved nor closed,
913
+ it also prints what the policy gate would decide now for the request's release on its
914
+ tenant (under `-o json`, as `policy_gate`). It needs `workspace:policy` read; without it, or with the Policies
915
+ feature off, it prints `Policy checks: not available (<why>)` (`policy_gate: null` and `policy_gate_unavailable`
916
+ under `-o json`) and the command still succeeds. The deploy an approval triggers, and a bypass deploy, are gated like
917
+ `tenant deploy_release`: a refusal prints the verdict and exits `2`. An approval refused this way is still recorded
918
+ and the request stays approved; deploy it with `tenant deploy_release --override-reason`. `--override-policy`
919
+ (with `--reason` as the override reason, and the `workspace:policy` update permission) proceeds past the gate on
920
+ `set_status --status approve` and on `bypass`. Approval bypass and policy override are separate permissions:
921
+ neither implies the other.
922
+
923
+ CI example (e.g. a GitHub Action step):
924
+
925
+ ```bash
926
+ xano tenant_deploy_request create "$PR_TITLE" --tenant prod --release v1.2 \
927
+ --description "$PR_BODY" --reviewers 12,45 -o json
928
+ ```
929
+
662
930
  #### Tenant License
663
931
 
664
932
  ```bash
@@ -63,6 +63,16 @@ export default abstract class BaseCommand extends Command {
63
63
  * Sets NODE_TLS_REJECT_UNAUTHORIZED=0 so all fetch() calls skip cert verification.
64
64
  */
65
65
  protected applyInsecureFromProfile(): void;
66
+ /**
67
+ * Report a failure as exit 1 for commands that reserve exit 2 for findings: operational errors,
68
+ * flag errors and oclif's own default exit 2 all become 1, and under `-o json` the failure is also
69
+ * written to stdout as `{"error": {"exit": 1, "message"}}`. A deliberate exit 0 passes through.
70
+ */
71
+ protected catchAsOperational(error: Error & {
72
+ oclif?: {
73
+ exit?: number;
74
+ };
75
+ }): Promise<void>;
66
76
  finally(_: Error | undefined): Promise<void>;
67
77
  /**
68
78
  * Get the resolved credentials file path, respecting --config flag and XANO_CONFIG env var.
@@ -78,6 +88,8 @@ export default abstract class BaseCommand extends Command {
78
88
  protected getOrCreateSandbox(profile: ProfileConfig, verbose: boolean): Promise<SandboxTenant>;
79
89
  protected getProfile(): string | undefined;
80
90
  init(): Promise<void>;
91
+ /** Raw-argv check: the parsed flags are not available yet when the banner prints. */
92
+ protected isJsonOutput(): boolean;
81
93
  protected loadCredentialsFile(): CredentialsFile | null;
82
94
  /**
83
95
  * Poll a static-host build until it reaches a terminal status (ok | error),
@@ -129,7 +141,6 @@ export default abstract class BaseCommand extends Command {
129
141
  verbose: boolean;
130
142
  workspaceId: string;
131
143
  }): Promise<string>;
132
- private isJsonOutput;
133
144
  /**
134
145
  * Find and parse the nearest project-local profile.yaml, unless an explicit
135
146
  * -p/XANO_PROFILE was given (in which case the local file is ignored).
@@ -156,6 +156,18 @@ export default class BaseCommand extends Command {
156
156
  // Don't fail the command if we can't read the profile for insecure check
157
157
  }
158
158
  }
159
+ /**
160
+ * Report a failure as exit 1 for commands that reserve exit 2 for findings: operational errors,
161
+ * flag errors and oclif's own default exit 2 all become 1, and under `-o json` the failure is also
162
+ * written to stdout as `{"error": {"exit": 1, "message"}}`. A deliberate exit 0 passes through.
163
+ */
164
+ async catchAsOperational(error) {
165
+ if (error.oclif?.exit === 0)
166
+ return super.catch(error);
167
+ if (this.isJsonOutput())
168
+ this.log(JSON.stringify({ error: { exit: 1, message: error.message } }, null, 2));
169
+ this.error(error, { exit: 1 });
170
+ }
159
171
  async finally(_) {
160
172
  if (this.updateNotice && !this.isJsonOutput()) {
161
173
  this.log(this.updateNotice);
@@ -221,6 +233,19 @@ export default class BaseCommand extends Command {
221
233
  const forceUpdateCheck = process.env.XANO_FORCE_UPDATE_CHECK === '1';
222
234
  this.updateNotice = checkForUpdate(this.config.version, forceUpdateCheck);
223
235
  }
236
+ /** Raw-argv check: the parsed flags are not available yet when the banner prints. */
237
+ isJsonOutput() {
238
+ const args = this.argv;
239
+ for (let i = 0; i < args.length; i++) {
240
+ if (args[i] === '--output' && args[i + 1] === 'json')
241
+ return true;
242
+ if (args[i] === '-o' && args[i + 1] === 'json')
243
+ return true;
244
+ if (args[i] === '--output=json' || args[i] === '-o=json')
245
+ return true;
246
+ }
247
+ return false;
248
+ }
224
249
  loadCredentialsFile() {
225
250
  const credentialsPath = this.getCredentialsPath();
226
251
  if (!fs.existsSync(credentialsPath)) {
@@ -354,18 +379,19 @@ export default class BaseCommand extends Command {
354
379
  ...(timeoutMs > 0 && !options.signal ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
355
380
  });
356
381
  const contentType = headers['Content-Type'] || 'application/json';
382
+ const logDiagnostic = this.isJsonOutput() ? this.logToStderr.bind(this) : this.log.bind(this);
357
383
  if (verbose) {
358
- this.log('');
359
- this.log('─'.repeat(60));
360
- this.log(`→ ${method} ${url}`);
361
- this.log(` Content-Type: ${contentType}`);
384
+ logDiagnostic('');
385
+ logDiagnostic('─'.repeat(60));
386
+ logDiagnostic(`→ ${method} ${url}`);
387
+ logDiagnostic(` Content-Type: ${contentType}`);
362
388
  if (authToken) {
363
- this.log(` Authorization: Bearer ${authToken.slice(0, 8)}...${authToken.slice(-4)}`);
389
+ logDiagnostic(` Authorization: Bearer ${authToken.slice(0, 8)}...${authToken.slice(-4)}`);
364
390
  }
365
391
  if (options.body) {
366
392
  const bodyStr = typeof options.body === 'string' ? options.body : String(options.body);
367
393
  const bodyPreview = bodyStr.length > 500 ? bodyStr.slice(0, 500) + '...' : bodyStr;
368
- this.log(` Body: ${bodyPreview}`);
394
+ logDiagnostic(` Body: ${bodyPreview}`);
369
395
  }
370
396
  }
371
397
  const startTime = Date.now();
@@ -381,7 +407,7 @@ export default class BaseCommand extends Command {
381
407
  if (dispatcherSupported && isInvalidDispatcherError(error)) {
382
408
  dispatcherSupported = false;
383
409
  if (verbose) {
384
- this.log(' (this Node runtime rejected undici dispatcher; retrying without it — see DEV-7773)');
410
+ logDiagnostic(' (this Node runtime rejected undici dispatcher; retrying without it — see DEV-7773)');
385
411
  }
386
412
  response = await fetch(url, buildFetchOptions(false));
387
413
  }
@@ -391,9 +417,9 @@ export default class BaseCommand extends Command {
391
417
  }
392
418
  const elapsed = Date.now() - startTime;
393
419
  if (verbose) {
394
- this.log(`← ${response.status} ${response.statusText} (${elapsed}ms)`);
395
- this.log('─'.repeat(60));
396
- this.log('');
420
+ logDiagnostic(`← ${response.status} ${response.statusText} (${elapsed}ms)`);
421
+ logDiagnostic('─'.repeat(60));
422
+ logDiagnostic('');
397
423
  }
398
424
  return response;
399
425
  }
@@ -468,18 +494,6 @@ export default class BaseCommand extends Command {
468
494
  conclude(`stopped waiting after ${Math.round(timeoutMs / 1000)}s (last status: ${stage || 'unknown'})`);
469
495
  return stage;
470
496
  }
471
- isJsonOutput() {
472
- const args = process.argv;
473
- for (let i = 0; i < args.length; i++) {
474
- if (args[i] === '--output' && args[i + 1] === 'json')
475
- return true;
476
- if (args[i] === '-o' && args[i + 1] === 'json')
477
- return true;
478
- if (args[i] === '--output=json' || args[i] === '-o=json')
479
- return true;
480
- }
481
- return false;
482
- }
483
497
  /**
484
498
  * Find and parse the nearest project-local profile.yaml, unless an explicit
485
499
  * -p/XANO_PROFILE was given (in which case the local file is ignored).
@@ -8,11 +8,33 @@ export default class BranchSetLive extends BaseCommand {
8
8
  static flags: {
9
9
  force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
10
10
  output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
12
  workspace: import("@oclif/core/interfaces").OptionFlag<number | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
14
15
  verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
15
16
  };
17
+ /** Set once the platform refuses for a weakened mandatory policy: already explained, it exits 1 as it is. */
18
+ private refusedByPermission;
19
+ /** Set once the set-live policy gate refuses, the one failure that exits 2. */
20
+ private refusedByPolicyGate;
21
+ /** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
22
+ protected catch(error: Error & {
23
+ oclif?: {
24
+ exit?: number;
25
+ };
26
+ }): Promise<void>;
16
27
  run(): Promise<void>;
17
28
  private confirm;
29
+ /**
30
+ * The set-live gate refused: the verdict on stdout (under `-o json`, `{set_live: false, message,
31
+ * policy_gate}`), then the platform's message and how to proceed, exiting 2.
32
+ */
33
+ private refuseSetLive;
34
+ /**
35
+ * The branch weakens a mandatory policy of the live branch (or lacks one), and the credential lacks
36
+ * `workspace:policy` update. That is a permission refusal, not a blocking finding: it exits 1 (under
37
+ * `-o json` as `{error}`) with the platform's sentence and how to go on. The live branch is unchanged.
38
+ */
39
+ private refuseWeakening;
18
40
  }