@ory/argus 0.13.0 → 0.13.2

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.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "2702eaeaad8d59305e3605a1543b47c6222b4967",
4
- "commitShort": "2702eae",
3
+ "commit": "4961e2084b50f7c37a1c44c93735998394afe12d",
4
+ "commitShort": "4961e20",
5
5
  "branch": "main",
6
- "commitDate": "2026-07-14T10:53:59-07:00",
6
+ "commitDate": "2026-07-14T13:13:40-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-07-14T17:57:49.846Z"
8
+ "builtAt": "2026-07-14T20:17:46.098Z"
9
9
  }
@@ -83,6 +83,7 @@ const tool_catalog_js_1 = require("./tool-catalog.js");
83
83
  const index_js_1 = require("./local/index.js");
84
84
  const subject_js_1 = require("./subject.js");
85
85
  const permissions_cli_js_1 = require("./permissions-cli.js");
86
+ const version_cli_js_1 = require("./version-cli.js");
86
87
  const ui = __importStar(require("./ui.js"));
87
88
  /** Loopback redirect URIs the PKCE user login expects on the OAuth2 client. */
88
89
  exports.LOOPBACK_REDIRECT_URIS = auth_js_1.LOOPBACK_PORTS.map((port) => `http://127.0.0.1:${port}/callback`);
@@ -155,6 +156,17 @@ async function installOryCli() {
155
156
  * open-coded as a local `postInstallPermissions` helper.
156
157
  */
157
158
  async function runPostInstall(binName, harness, args = [], deps = {}) {
159
+ // Surface the running CLI version up front. `npx -p @ory/<harness>` (no
160
+ // version pin) will happily reuse a previously-cached install rather than
161
+ // re-resolve to latest, so a user can silently be running an old CLI whose
162
+ // install flow predates newer behavior (e.g. the interactive setup wizard)
163
+ // while believing they're on the current release. Printing the version makes
164
+ // that mismatch visible at a glance; the README's troubleshooting note
165
+ // explains how to clear a stale npx cache.
166
+ const cliVersion = (0, version_cli_js_1.resolveCoreVersion)();
167
+ if (cliVersion !== "unknown") {
168
+ ui.info(`${binName} v${cliVersion}`);
169
+ }
158
170
  const result = await runInteractiveSetup(binName, harness, args, deps);
159
171
  // The Ory Network wizard path bootstraps permissions itself (via the
160
172
  // admin-authenticated `ory` CLI). Only fall back to the DCR-token bootstrap
@@ -638,15 +650,20 @@ async function completeFullSetup(binName, harness, ctx) {
638
650
  * The tuples reference the permission namespace (default `AgentTools`), which
639
651
  * must be defined in the project's permission model or the write fails with
640
652
  * `NotFound`. So we inspect the model first (see {@link inspectPermissionModel})
641
- * and act on what's actually there:
653
+ * and act on what's actually there. We provision in every case *except* when
654
+ * the namespace already exists:
642
655
  *
656
+ * - **Namespace already defined** → grant directly, touch nothing.
643
657
  * - **No model yet** (a freshly created Ory Network project) → offer to
644
658
  * provision a minimal `<namespace>` model (`ory update opl`), then grant.
645
- * - **Namespace already defined**grant directly.
646
- * - **Model exists but defines only *other* namespaces** we can't
647
- * auto-provision, because `ory update opl` overwrites the whole OPL and
648
- * would clobber the existing model. Print the exact snippet to add and skip
649
- * the grant (it would only `NotFound`).
659
+ * - **Model exists (OPL source fetchable) but lacks `<namespace>`** offer
660
+ * to *merge* the namespace into the existing OPL and re-upload it, leaving
661
+ * the other namespaces untouched, then grant. Ory Network serves the OPL
662
+ * source at a `location` URL in the permission config, which makes this
663
+ * non-destructive merge possible.
664
+ * - **Model provably exists but its source couldn't be read** → we can't
665
+ * merge and `ory update opl` overwrites the whole OPL, so we refuse to
666
+ * clobber it: print the exact snippet to add and skip the grant.
650
667
  *
651
668
  * Best-effort throughout: any failure prints a short, actionable note rather
652
669
  * than a wall of errors. Observe mode (the install default) keeps tools working
@@ -658,10 +675,36 @@ async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, p
658
675
  if (tools.length === 0)
659
676
  return;
660
677
  // Ensure the namespace the tuples reference exists, or the write NotFounds.
661
- const modelState = inspectPermissionModel(runner, projectId, namespace);
662
- if (modelState === "empty") {
663
- // No model yet safe to provision a minimal one (prompted).
664
- ui.heading(`This project has no permission model, so '${namespace}' isn't defined yet —`);
678
+ // We only skip provisioning when the namespace is already defined; every
679
+ // other state provisions. When we can fetch the existing OPL source we merge
680
+ // our namespace into it (non-destructive); otherwise we create a fresh
681
+ // minimal model. The one case we refuse is a model that provably exists but
682
+ // whose source we couldn't read — overwriting it would clobber it.
683
+ const model = await inspectPermissionModel(runner, projectId, namespace);
684
+ if (model.kind === "merge") {
685
+ // A model exists and we have its OPL source — append our namespace without
686
+ // disturbing the others (prompted).
687
+ ui.heading(`This project has a permission model, but it doesn't define '${namespace}'.`);
688
+ ui.hint("It can be added to the existing model without changing the other namespaces.");
689
+ ui.blank();
690
+ const answer = await prompt(ui.promptLine(`Add '${namespace}' to this project's permission model now?`, {
691
+ hint: "[Y/n]",
692
+ }));
693
+ if (isNo(answer)) {
694
+ printPermissionModelHelp(namespace);
695
+ return;
696
+ }
697
+ if (!provisionPermissionModel(runner, projectId, namespace, model.opl)) {
698
+ ui.warning(`Could not add '${namespace}' to the permission model automatically.`);
699
+ printPermissionModelHelp(namespace);
700
+ return;
701
+ }
702
+ ui.success(`Added '${namespace}' to the permission model.`);
703
+ }
704
+ else if (model.kind === "empty" || model.kind === "unknown") {
705
+ // No model (or one we couldn't read at all) — provision a minimal one
706
+ // (prompted). Reading the config failing is treated as "nothing there".
707
+ ui.heading(`This project has no '${namespace}' permission model yet —`);
665
708
  ui.hint("without it, permission checks can't be granted or enforced.");
666
709
  ui.blank();
667
710
  const answer = await prompt(ui.promptLine(`Create a minimal '${namespace}' permission model on this project now?`, { hint: "[Y/n]" }));
@@ -676,16 +719,15 @@ async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, p
676
719
  }
677
720
  ui.success(`Created the '${namespace}' permission model.`);
678
721
  }
679
- else if (modelState === "missing") {
680
- // A model exists but defines other namespaces provisioning would
681
- // overwrite it, so guide the user instead of clobbering their model.
682
- ui.heading(`This project has a permission model, but it doesn't define '${namespace}'.`);
683
- ui.hint("Adding it automatically would overwrite the existing model, so we won't.");
722
+ else if (model.kind === "blocked") {
723
+ // A model provably exists but we couldn't fetch its source, so we can't
724
+ // merge and `ory update opl` would overwrite it. Guide instead of clobber.
725
+ ui.heading(`This project has a permission model, but it doesn't define '${namespace}'`);
726
+ ui.hint("and its source couldn't be read, so adding it automatically might overwrite it.");
684
727
  printPermissionModelHelp(namespace);
685
728
  return;
686
729
  }
687
- // "present" (namespace already defined) or "unknown" (couldn't read the
688
- // model) fall through and attempt the grant best-effort.
730
+ // "present" (namespace already defined) falls through and grants directly.
689
731
  const subjectPatch = "subjectSet" in subject
690
732
  ? {
691
733
  subject_set: {
@@ -715,26 +757,13 @@ async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, p
715
757
  printPermissionModelHelp(namespace);
716
758
  }
717
759
  /**
718
- * Inspect the project's permission model to decide how to make `<namespace>`
719
- * grantable:
720
- *
721
- * - `present` — the namespace is already defined; grant directly.
722
- * - `empty` — the project has no model at all. A freshly created Ory
723
- * Network project reports this as an empty list, a `null`, or
724
- * an absent `namespaces` key — all of which mean "nothing
725
- * defined yet", so provisioning a minimal model is safe.
726
- * - `missing` — a model exists and defines *other* namespaces but not this
727
- * one. `ory update opl` overwrites the whole OPL, so we must
728
- * not auto-provision (it would clobber the existing model).
729
- * - `unknown` — the config couldn't be read/parsed; make no assumptions and
730
- * attempt the grant best-effort.
731
- *
732
- * The earlier version only recognized the exact `namespaces: []` shape as
733
- * "empty", so a fresh project reporting `null`/absent namespaces (or one that
734
- * already had unrelated namespaces) silently skipped provisioning and the grant
735
- * then failed with `NotFound`.
760
+ * Inspect the project's permission model. Ory Network reports the config in one
761
+ * of two shapes: `{ namespaces: [] }` (or `null`) for a project with no model,
762
+ * and `{ namespaces: { location: "https://….txt" } }` for an OPL-configured
763
+ * onewhere the `.txt` is the full OPL *source*. We fetch that source so a
764
+ * missing namespace can be merged in rather than clobbering the whole model.
736
765
  */
737
- function inspectPermissionModel(runner, projectId, namespace) {
766
+ async function inspectPermissionModel(runner, projectId, namespace) {
738
767
  const r = runner.exec([
739
768
  "get",
740
769
  "permission-config",
@@ -744,47 +773,75 @@ function inspectPermissionModel(runner, projectId, namespace) {
744
773
  "json",
745
774
  ]);
746
775
  if (r.status !== 0)
747
- return "unknown";
776
+ return { kind: "unknown" };
748
777
  const cfg = safeJsonParse(r.stdout);
749
778
  if (!cfg || typeof cfg !== "object")
750
- return "unknown";
779
+ return { kind: "unknown" };
751
780
  const ns = cfg.namespaces;
752
- // A fresh project reports no model as null / an absent key / an empty list.
781
+ // A fresh project reports no model as null / an absent key.
753
782
  if (ns == null)
754
- return "empty";
755
- if (!Array.isArray(ns))
756
- return "unknown";
757
- if (ns.length === 0)
758
- return "empty";
759
- const names = ns
760
- .map((n) => n && typeof n === "object"
761
- ? n.name
762
- : undefined)
763
- .filter((n) => typeof n === "string");
764
- return names.includes(namespace) ? "present" : "missing";
783
+ return { kind: "empty" };
784
+ // OPL-configured form: { namespaces: { location: "https://….txt" } }.
785
+ // The .txt holds the full OPL source, so we can merge into it.
786
+ if (!Array.isArray(ns) && typeof ns === "object") {
787
+ const location = ns.location;
788
+ if (typeof location !== "string" || !location)
789
+ return { kind: "empty" };
790
+ const opl = await fetchOplSource(location);
791
+ // A model exists but its source is unreadable — can't merge, mustn't clobber.
792
+ if (opl == null)
793
+ return { kind: "blocked" };
794
+ return oplDefinesClass(opl, namespace)
795
+ ? { kind: "present" }
796
+ : { kind: "merge", opl };
797
+ }
798
+ // Inline compiled form: { namespaces: [{ name, id }, …] } (legacy / self-hosted).
799
+ // We only have names, not source, so a missing namespace can't be merged.
800
+ if (Array.isArray(ns)) {
801
+ if (ns.length === 0)
802
+ return { kind: "empty" };
803
+ const names = ns
804
+ .map((n) => n && typeof n === "object"
805
+ ? n.name
806
+ : undefined)
807
+ .filter((n) => typeof n === "string");
808
+ return names.includes(namespace) ? { kind: "present" } : { kind: "blocked" };
809
+ }
810
+ return { kind: "unknown" };
811
+ }
812
+ /** Fetch the OPL source served at a permission-config `location` URL. */
813
+ async function fetchOplSource(url) {
814
+ try {
815
+ const res = await fetch(url);
816
+ if (!res.ok)
817
+ return null;
818
+ return await res.text();
819
+ }
820
+ catch {
821
+ return null;
822
+ }
823
+ }
824
+ /** Whether an OPL source defines `class <name>`. */
825
+ function oplDefinesClass(opl, name) {
826
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
827
+ return new RegExp(`\\bclass\\s+${escaped}\\b`).test(opl);
765
828
  }
766
829
  /**
767
- * Provision a minimal permission model that defines `<namespace>` with a
768
- * `use` relation, via `ory update opl`. The namespace becomes an OPL class, so
769
- * it must be a valid identifier; if not, we bail (caller guides instead).
770
- * Writes the OPL to a temp file because `ory update opl` reads `--file` only
771
- * (no stdin). Returns true on success.
830
+ * Provision a permission model that defines `<namespace>` with a `use`
831
+ * relation, via `ory update opl`. The namespace becomes an OPL class, so it
832
+ * must be a valid identifier; if not, we bail (caller guides instead).
833
+ *
834
+ * When `existingOpl` is given, the namespace is appended to that source
835
+ * (preserving the model's other namespaces); otherwise a fresh minimal model
836
+ * is generated. Writes the OPL to a temp file because `ory update opl` reads
837
+ * `--file` only (no stdin). Returns true on success.
772
838
  */
773
- function provisionPermissionModel(runner, projectId, namespace) {
839
+ function provisionPermissionModel(runner, projectId, namespace, existingOpl) {
774
840
  if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace))
775
841
  return false;
776
- const opl = [
777
- 'import { Namespace } from "@ory/permission-namespace-types"',
778
- "",
779
- "class User implements Namespace {}",
780
- "",
781
- `class ${namespace} implements Namespace {`,
782
- " related: {",
783
- " use: User[]",
784
- " }",
785
- "}",
786
- "",
787
- ].join("\n");
842
+ const opl = existingOpl !== undefined
843
+ ? mergeNamespaceIntoOpl(existingOpl, namespace)
844
+ : freshOpl(namespace);
788
845
  let dir;
789
846
  try {
790
847
  dir = fs.mkdtempSync(path.join(os.tmpdir(), "ory-opl-"));
@@ -820,6 +877,39 @@ function provisionPermissionModel(runner, projectId, namespace) {
820
877
  }
821
878
  }
822
879
  }
880
+ /** A minimal standalone OPL defining `User` and `<namespace>` with `use`. */
881
+ function freshOpl(namespace) {
882
+ return [
883
+ 'import { Namespace } from "@ory/permission-namespace-types"',
884
+ "",
885
+ "class User implements Namespace {}",
886
+ "",
887
+ `class ${namespace} implements Namespace {`,
888
+ " related: {",
889
+ " use: User[]",
890
+ " }",
891
+ "}",
892
+ "",
893
+ ].join("\n");
894
+ }
895
+ /**
896
+ * Append a `<namespace>` class (with a `use: User[]` relation) to an existing
897
+ * OPL source, leaving every other namespace untouched. Adds a `User` class too
898
+ * when the source doesn't already define one, since `use` is typed `User[]`.
899
+ */
900
+ function mergeNamespaceIntoOpl(existingOpl, namespace) {
901
+ let opl = existingOpl.replace(/\s*$/, "\n");
902
+ if (!oplDefinesClass(opl, "User")) {
903
+ opl += "\nclass User implements Namespace {}\n";
904
+ }
905
+ opl +=
906
+ `\nclass ${namespace} implements Namespace {\n` +
907
+ " related: {\n" +
908
+ " use: User[]\n" +
909
+ " }\n" +
910
+ "}\n";
911
+ return opl;
912
+ }
823
913
  function printPermissionModelHelp(namespace) {
824
914
  // When the namespace is a valid OPL class name, show the exact snippet to
825
915
  // paste into the model — that's the concrete "how" behind "define it". The
@@ -26,6 +26,13 @@ export interface BuildInfo {
26
26
  * `unknown`.
27
27
  */
28
28
  export declare function resolveBuildInfo(): BuildInfo;
29
+ /**
30
+ * Resolve the `@ory/argus` core version from this module's own package.json.
31
+ * Exported so the install flow can surface the running CLI version — every
32
+ * `@ory/*` package releases in lockstep, so the core version is the release
33
+ * version of whatever plugin CLI is executing.
34
+ */
35
+ export declare function resolveCoreVersion(): string;
29
36
  export interface VersionInfo {
30
37
  plugin: {
31
38
  name: string;
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.resolveBuildInfo = resolveBuildInfo;
37
+ exports.resolveCoreVersion = resolveCoreVersion;
37
38
  exports.collectVersionInfo = collectVersionInfo;
38
39
  exports.runVersionCommand = runVersionCommand;
39
40
  /**
@@ -158,7 +159,12 @@ function readPackageMeta(packageRoot) {
158
159
  return { name: "unknown", version: "0.0.0" };
159
160
  }
160
161
  }
161
- /** Resolve the `@ory/argus` core version from this module's own package.json. */
162
+ /**
163
+ * Resolve the `@ory/argus` core version from this module's own package.json.
164
+ * Exported so the install flow can surface the running CLI version — every
165
+ * `@ory/*` package releases in lockstep, so the core version is the release
166
+ * version of whatever plugin CLI is executing.
167
+ */
162
168
  function resolveCoreVersion() {
163
169
  let dir = __dirname;
164
170
  for (let i = 0; i < 5; i++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",