@ory/argus 0.13.9 → 1.0.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 (133) hide show
  1. package/README.md +31 -45
  2. package/assets/commands/temporal-up.md +1 -1
  3. package/assets/skills/auth-setup/SKILL.md +1 -1
  4. package/assets/skills/local-dev/SKILL.md +17 -7
  5. package/assets/skills/ory-build-agent/SKILL.md +43 -97
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -18
  7. package/assets/skills/ory-temporal-worker/SKILL.md +38 -42
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +93 -30
  10. package/dist/adapters.js +464 -136
  11. package/dist/agent-auth.d.ts +258 -68
  12. package/dist/agent-auth.js +998 -202
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +40 -4
  16. package/dist/auth.js +247 -19
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +128 -0
  20. package/dist/branding.js +151 -0
  21. package/dist/build-info.json +4 -4
  22. package/dist/cli-invocation.d.ts +1 -1
  23. package/dist/cli-invocation.js +2 -1
  24. package/dist/cli.d.ts +20 -29
  25. package/dist/cli.js +271 -278
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +249 -57
  29. package/dist/config.js +486 -62
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +8 -8
  33. package/dist/contract-suite.js +88 -69
  34. package/dist/denial.d.ts +36 -3
  35. package/dist/denial.js +79 -10
  36. package/dist/event-reporter.d.ts +77 -0
  37. package/dist/event-reporter.js +776 -0
  38. package/dist/external-registrations-main.d.ts +10 -0
  39. package/dist/external-registrations-main.js +38 -0
  40. package/dist/external-registrations.d.ts +79 -0
  41. package/dist/external-registrations.js +188 -0
  42. package/dist/help-cli.d.ts +39 -0
  43. package/dist/help-cli.js +55 -0
  44. package/dist/hook-timeout.d.ts +64 -0
  45. package/dist/hook-timeout.js +88 -0
  46. package/dist/index.d.ts +31 -19
  47. package/dist/index.js +182 -31
  48. package/dist/lifecycle.d.ts +3 -3
  49. package/dist/lifecycle.js +38 -6
  50. package/dist/local/cli.js +11 -6
  51. package/dist/local/configs.d.ts +74 -18
  52. package/dist/local/configs.js +291 -84
  53. package/dist/local/health.d.ts +14 -0
  54. package/dist/local/health.js +50 -4
  55. package/dist/local/index.d.ts +2 -2
  56. package/dist/local/index.js +24 -10
  57. package/dist/local/manager.d.ts +20 -1
  58. package/dist/local/manager.js +160 -39
  59. package/dist/local/ports.d.ts +158 -0
  60. package/dist/local/ports.js +443 -0
  61. package/dist/local/seed.d.ts +22 -25
  62. package/dist/local/seed.js +88 -56
  63. package/dist/logger.d.ts +54 -25
  64. package/dist/logger.js +329 -63
  65. package/dist/mcp.d.ts +2 -2
  66. package/dist/mcp.js +10 -5
  67. package/dist/mirror-bootstrap.d.ts +48 -0
  68. package/dist/mirror-bootstrap.js +254 -0
  69. package/dist/opl.d.ts +289 -0
  70. package/dist/opl.js +446 -0
  71. package/dist/permission-mode.d.ts +87 -0
  72. package/dist/permission-mode.js +307 -0
  73. package/dist/permissions-cli.d.ts +13 -49
  74. package/dist/permissions-cli.js +154 -348
  75. package/dist/permissions.d.ts +148 -38
  76. package/dist/permissions.js +591 -45
  77. package/dist/post-install.d.ts +33 -0
  78. package/dist/post-install.js +127 -0
  79. package/dist/read-credential.d.ts +65 -0
  80. package/dist/read-credential.js +86 -0
  81. package/dist/registry/cli.js +5 -2
  82. package/dist/registry/config.d.ts +0 -17
  83. package/dist/registry/config.js +0 -23
  84. package/dist/registry/index.d.ts +1 -1
  85. package/dist/registry/index.js +2 -2
  86. package/dist/registry/manager.d.ts +4 -21
  87. package/dist/registry/manager.js +83 -55
  88. package/dist/runtime-credential.d.ts +140 -0
  89. package/dist/runtime-credential.js +572 -0
  90. package/dist/runtime.d.ts +408 -0
  91. package/dist/runtime.js +748 -0
  92. package/dist/setup.d.ts +23 -28
  93. package/dist/setup.js +57 -84
  94. package/dist/status-cli.d.ts +29 -13
  95. package/dist/status-cli.js +124 -144
  96. package/dist/status-data.d.ts +195 -0
  97. package/dist/status-data.js +333 -0
  98. package/dist/status-system.d.ts +24 -0
  99. package/dist/status-system.js +56 -0
  100. package/dist/subject.d.ts +126 -20
  101. package/dist/subject.js +215 -30
  102. package/dist/testing.d.ts +74 -38
  103. package/dist/testing.js +185 -68
  104. package/dist/tool-catalog.d.ts +53 -11
  105. package/dist/tool-catalog.js +164 -13
  106. package/dist/tool-metadata.d.ts +7 -6
  107. package/dist/tool-metadata.js +6 -5
  108. package/dist/types.d.ts +11 -1
  109. package/dist/uninstall.d.ts +74 -19
  110. package/dist/uninstall.js +224 -49
  111. package/dist/user-login.d.ts +22 -16
  112. package/dist/user-login.js +67 -96
  113. package/dist/watch-cli.d.ts +6 -0
  114. package/dist/watch-cli.js +217 -0
  115. package/package.json +3 -11
  116. package/dist/dev.d.ts +0 -103
  117. package/dist/dev.js +0 -584
  118. package/dist/interactive-setup.d.ts +0 -165
  119. package/dist/interactive-setup.js +0 -1546
  120. package/dist/local/jaeger-main.d.ts +0 -13
  121. package/dist/local/jaeger-main.js +0 -85
  122. package/dist/local/jaeger.d.ts +0 -50
  123. package/dist/local/jaeger.js +0 -162
  124. package/dist/otel/exporter.d.ts +0 -17
  125. package/dist/otel/exporter.js +0 -12
  126. package/dist/otel/index.d.ts +0 -2
  127. package/dist/otel/index.js +0 -8
  128. package/dist/otel/otlp.d.ts +0 -103
  129. package/dist/otel/otlp.js +0 -385
  130. package/dist/tracer.d.ts +0 -190
  131. package/dist/tracer.js +0 -481
  132. package/dist/watch-sandbox.d.ts +0 -9
  133. package/dist/watch-sandbox.js +0 -81
@@ -1,76 +1,35 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Shared `permissions` CLI implementation. Each harness's `cli/main.ts`
4
- * delegates to {@link runPermissionsCommand}, passing the harness's name
5
- * so subcommands can scope to that harness's tool catalog.
4
+ * delegates to {@link runPermissionsCommand}, passing the harness's name so the
5
+ * report can scope to that harness's tool catalog.
6
6
  *
7
- * Subcommands:
7
+ * `permissions` takes **no subcommand** — it is a single read-only report: the
8
+ * live permission mode (read from the Ory project), the subject being checked,
9
+ * and the allow / deny / blocked breakdown across the harness's known tool
10
+ * catalog. (`permissions status` is still accepted; it was the name through
11
+ * v0.14, and the report is the same.)
8
12
  *
9
- * - `status` — Print the configured {@link PermissionMode} and, when
10
- * the client can reach Ory, the allow/deny breakdown
11
- * across the harness's known tool catalog.
12
- * - `bootstrap`Idempotently write `<namespace>:<tool>#use@<userSubject>`
13
- * tuples for every tool in the harness's catalog. Run
14
- * automatically by `install` when a user identity is
15
- * cached; can be re-run by hand any time.
16
- * - `observe` — Persist `permissionMode = "observe"` in the shared
17
- * config (denies log but don't block).
18
- * - `enforce` — Persist `permissionMode = "enforce"` in the shared
19
- * config (denies block — the production posture).
20
- *
21
- * All four are non-destructive on existing config and idempotent on
22
- * Keto: re-running them is always safe.
13
+ * Everything that *writes* to the project is provisioned in the **Ory Console**
14
+ * (Agent Security), not from the plugin: the permission model, the per-tool
15
+ * grants, explicit blocks, and the observe/enforce posture. That is what lets a
16
+ * plugin be installed with nothing but a project URL the public OAuth2 client
17
+ * id has a reserved default, with no workspace privilege, project API key, or admin write path. The
18
+ * plugin is a read-only client of the project's permission state.
23
19
  */
24
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
- if (k2 === undefined) k2 = k;
26
- var desc = Object.getOwnPropertyDescriptor(m, k);
27
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
- desc = { enumerable: true, get: function() { return m[k]; } };
29
- }
30
- Object.defineProperty(o, k2, desc);
31
- }) : (function(o, m, k, k2) {
32
- if (k2 === undefined) k2 = k;
33
- o[k2] = m[k];
34
- }));
35
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
- Object.defineProperty(o, "default", { enumerable: true, value: v });
37
- }) : function(o, v) {
38
- o["default"] = v;
39
- });
40
- var __importStar = (this && this.__importStar) || (function () {
41
- var ownKeys = function(o) {
42
- ownKeys = Object.getOwnPropertyNames || function (o) {
43
- var ar = [];
44
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
- return ar;
46
- };
47
- return ownKeys(o);
48
- };
49
- return function (mod) {
50
- if (mod && mod.__esModule) return mod;
51
- var result = {};
52
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
- __setModuleDefault(result, mod);
54
- return result;
55
- };
56
- })();
57
20
  Object.defineProperty(exports, "__esModule", { value: true });
58
21
  exports.runPermissionsCommand = runPermissionsCommand;
59
- exports.isUserIdentityCached = isUserIdentityCached;
60
- exports.printPermissionsOnboardingHelp = printPermissionsOnboardingHelp;
61
- exports.maybeAutoBootstrap = maybeAutoBootstrap;
62
22
  const config_js_1 = require("./config.js");
23
+ const permission_mode_js_1 = require("./permission-mode.js");
63
24
  const client_js_1 = require("./client.js");
64
25
  const logger_js_1 = require("./logger.js");
65
26
  const agent_auth_js_1 = require("./agent-auth.js");
66
- const auth_store_js_1 = require("./auth-store.js");
67
27
  const tool_catalog_js_1 = require("./tool-catalog.js");
68
28
  const subject_js_1 = require("./subject.js");
69
- const cli_invocation_js_1 = require("./cli-invocation.js");
70
- const ui = __importStar(require("./ui.js"));
71
- const ENV_PERMISSION_MODE = "ORY_PERMISSION_MODE";
29
+ const permissions_js_1 = require("./permissions.js");
30
+ const opl_js_1 = require("./opl.js");
72
31
  function resolveNamespace() {
73
- return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
32
+ return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTool";
74
33
  }
75
34
  /**
76
35
  * Entry point invoked by each harness CLI's `permissions` case.
@@ -81,23 +40,28 @@ function resolveNamespace() {
81
40
  */
82
41
  async function runPermissionsCommand(binName, harness, args) {
83
42
  const sub = args[0];
84
- if (!sub || sub === "--help" || sub === "-h") {
43
+ if (sub === "--help" || sub === "-h") {
85
44
  printPermissionsHelp(binName);
86
- return sub ? 0 : 1;
45
+ return 0;
46
+ }
47
+ // No subcommand is the command: print the report. The report spins up a client
48
+ // and probes per tool, emitting debug/info events — quiet the stderr firehose
49
+ // so those don't interleave with the human-facing output under
50
+ // ORY_AGENT_DEBUG=true; the log file still captures everything.
51
+ //
52
+ // `status` is accepted as the v0.14 spelling of the same thing.
53
+ if (!sub || sub === "status") {
54
+ return await (0, logger_js_1.withQuietStderr)(() => runPermissionsStatus(binName, harness));
87
55
  }
88
56
  switch (sub) {
89
- // `status` and `bootstrap` spin up a client and probe/grant per tool,
90
- // emitting debug/info events. Quiet the stderr firehose so those don't
91
- // interleave with the command's human-facing output under
92
- // ORY_AGENT_DEBUG=true; the log file still captures everything.
93
- case "status":
94
- return await (0, logger_js_1.withQuietStderr)(() => runPermissionsStatus(binName, harness));
95
57
  case "bootstrap":
96
- return await (0, logger_js_1.withQuietStderr)(() => runPermissionsBootstrap(binName, harness, args.slice(1)));
58
+ case "block":
59
+ case "unblock":
97
60
  case "observe":
98
- return runPermissionsSetMode(binName, "observe");
99
61
  case "enforce":
100
- return runPermissionsSetMode(binName, "enforce");
62
+ case "install-schema":
63
+ printProvisioningMovedToConsole(binName, sub);
64
+ return 1;
101
65
  default:
102
66
  console.error(`Unknown permissions subcommand: ${sub}`);
103
67
  console.error(`Run "${binName} permissions --help" for usage.`);
@@ -105,39 +69,40 @@ async function runPermissionsCommand(binName, harness, args) {
105
69
  }
106
70
  }
107
71
  function printPermissionsHelp(binName) {
108
- console.log(`Usage: ${binName} permissions <subcommand>`);
72
+ console.log(`Usage: ${binName} permissions`);
109
73
  console.log("");
110
- console.log("Subcommands:");
111
- console.log(" status Show permission mode and tool coverage for the current user");
112
- console.log(" bootstrap Grant the current user 'use' on every built-in tool (idempotent)");
113
- console.log(" observe Switch to observe mode (denies log but don't block)");
114
- console.log(" enforce Switch to enforce mode (denies block — production posture)");
74
+ console.log("Prints the live permission mode, the subject being checked, and the");
75
+ console.log("allow / deny / blocked breakdown across this harness's built-in tools.");
76
+ console.log("Read-only it changes nothing.");
115
77
  console.log("");
116
78
  console.log("Permission mode controls what happens when an Ory check returns deny:");
117
- console.log(" observe — Log + audit-span, allow the tool through. The onboarding default.");
79
+ console.log(" observe — Log an audit event and allow the tool through. The default.");
118
80
  console.log(" enforce — Block the tool. The production posture.");
119
81
  console.log("");
120
- console.log(`Mode is read from ${ENV_PERMISSION_MODE} env, then the shared config file.`);
121
- }
122
- function runPermissionsSetMode(binName, mode) {
123
- (0, config_js_1.saveConfig)({ permissionMode: mode });
124
- const configPath = (0, config_js_1.getConfigPath)();
125
- console.log(`Permission mode set to '${mode}'.`);
126
- console.log(` Saved to: ${configPath}`);
82
+ console.log("The plugin only ever *reads* permissions. Tool grants, explicit blocks,");
83
+ console.log("the permission model, and the observe/enforce posture are all managed in");
84
+ console.log("the Ory Console (Agent Security) by someone with access to the project.");
127
85
  console.log("");
128
- if (mode === "observe") {
129
- console.log("Denies will be logged and recorded as `permission.observe_deny` spans,");
130
- console.log("but tools will be allowed through. Run");
131
- console.log(` ${binName} permissions enforce`);
132
- console.log("once your permission set is correct.");
133
- }
134
- else {
135
- console.log("Denies will block tool execution. The plugin is now enforcing.");
136
- console.log(`Switch back with: ${binName} permissions observe`);
137
- }
138
- console.log("");
139
- console.log(`Note: the ${ENV_PERMISSION_MODE} environment variable, when set, overrides this.`);
140
- return 0;
86
+ console.log(`A subject is in 'enforce' when granted ${opl_js_1.PERMISSION_MODE_NAMESPACE}:${opl_js_1.PERMISSION_MODE_OBJECT}#${opl_js_1.RELATION_ENFORCED_SUBJECTS},`);
87
+ console.log("otherwise 'observe'.");
88
+ }
89
+ /**
90
+ * The subcommands that used to write to the project — grants, explicit blocks,
91
+ * the deny posture, and the permission-model installer. All of that is now
92
+ * provisioned in the Ory Console, so these print where to go and exit non-zero
93
+ * rather than silently doing nothing.
94
+ */
95
+ function printProvisioningMovedToConsole(binName, attempted) {
96
+ console.error(`'permissions ${attempted}' is no longer part of the plugin.`);
97
+ console.error("");
98
+ console.error("Permissions are provisioned in the Ory Console (Agent Security):");
99
+ console.error(" - the permission model applied to the project");
100
+ console.error(" - per-tool grants for a user, agent, or group");
101
+ console.error(" - explicit blocks (a deny that beats any grant)");
102
+ console.error(" - the observe / enforce posture, project-wide or per principal");
103
+ console.error("");
104
+ console.error("The plugin reads that state and enforces it. To see what it resolves:");
105
+ console.error(` ${binName} permissions`);
141
106
  }
142
107
  async function runPermissionsStatus(binName, harness) {
143
108
  const resolved = (0, config_js_1.resolveConfig)();
@@ -146,20 +111,18 @@ async function runPermissionsStatus(binName, harness) {
146
111
  console.log(`Permission status (${harness})`);
147
112
  console.log("==========================================");
148
113
  console.log("");
149
- console.log(`Mode: ${resolved.permissionMode}${formatModeSource(resolved.permissionModeSource)}`);
150
- console.log(` (other modes: ${otherModes(resolved.permissionMode).join(", ")})`);
151
114
  console.log(`Namespace: ${namespace}`);
152
115
  console.log(`Project URL: ${resolved.projectUrl ?? "(not set)"}${formatSourceSuffix(resolved.projectUrlSource)}`);
153
- console.log(`Audit-only: ${resolved.auditOnly ? "yes (Ory disabled)" : "no"}`);
116
+ console.log(`OAuth2 Client: ${resolved.oauth2ClientId ?? "(not set)"}${formatSourceSuffix(resolved.oauth2ClientIdSource)}`);
117
+ console.log(`Agent Security: ${resolved.security.connected ? "connected" : `not connected (${(0, config_js_1.describeSecurityGap)(resolved.security)})`}`);
118
+ console.log(`Permission model: checks route through the '${opl_js_1.PERMIT_USE}' permit (explicit blocks honored)`);
154
119
  console.log("");
155
- if (resolved.auditOnly) {
156
- console.log("Ory is disabled (audit-only). No permission checks run.");
157
- console.log(`Re-enable with: ${binName} configure --project-url <URL> --oauth2-client-id <CLIENT_ID> [--api-key <KEY>]`);
158
- return 0;
159
- }
160
- if (!resolved.projectUrl) {
161
- console.log("No project URL configured — cannot probe permission coverage.");
162
- console.log(`Configure first: ${binName} configure --project-url <URL> --oauth2-client-id <CLIENT_ID> [--api-key <KEY>]`);
120
+ if (!resolved.security.connected) {
121
+ console.log("Ory Agent Security is not connected, so no permission checks run and there");
122
+ console.log("is no coverage to report. The rest of the plugin is unaffected.");
123
+ console.log("");
124
+ console.log(`Connect with: ${binName} configure --project-url <URL>`);
125
+ console.log("The project URL comes from the Ory Console (Agent Security).");
163
126
  return 0;
164
127
  }
165
128
  if (catalog.length === 0) {
@@ -179,12 +142,22 @@ async function runPermissionsStatus(binName, harness) {
179
142
  console.log("ORY_USER_SUBJECT_ID to probe against a known subject.");
180
143
  return 0;
181
144
  }
145
+ // Resolve the LIVE mode (a Keto read) rather than the cached config value, so
146
+ // status reflects exactly what the gate would decide.
147
+ const agentSubject = (0, subject_js_1.resolveAgentSubject)(client);
148
+ const live = await (0, permission_mode_js_1.resolvePermissionMode)(client, subject, {
149
+ additionalSubjects: agentSubject ? [{ scope: "agent", subject: agentSubject }] : [],
150
+ });
182
151
  console.log(`Subject: ${subjectId}`);
152
+ console.log(`Mode: ${live.mode}${formatModeSource(live.source, resolved.permissionModeCache?.fetchedAt)}`);
153
+ console.log(` (other modes: ${otherModes(live.mode).join(", ")})`);
154
+ console.log(` Set in the Ory Console (Agent Security): grant ${opl_js_1.PERMISSION_MODE_NAMESPACE}:${opl_js_1.PERMISSION_MODE_OBJECT}#${opl_js_1.RELATION_ENFORCED_SUBJECTS} to enforce.`);
183
155
  console.log(`Tool catalog: ${catalog.length} tools`);
184
156
  console.log("");
185
157
  const results = await probeCatalog(client, catalog, namespace, subject);
186
- const allowed = results.filter((r) => r.allowed).length;
187
- const denied = results.filter((r) => r.error === undefined && !r.allowed).length;
158
+ const allowed = results.filter((r) => r.error === undefined && r.allowed).length;
159
+ const blocked = results.filter((r) => r.blocked).length;
160
+ const denied = results.filter((r) => r.error === undefined && !r.allowed && !r.blocked).length;
188
161
  const errored = results.filter((r) => r.error !== undefined).length;
189
162
  const widest = Math.max(...catalog.map((t) => t.length), 4);
190
163
  console.log(` ${"Tool".padEnd(widest)} Status`);
@@ -192,34 +165,55 @@ async function runPermissionsStatus(binName, harness) {
192
165
  for (const r of results) {
193
166
  const status = r.error
194
167
  ? `error (${r.error})`
195
- : r.allowed
196
- ? "allowed"
197
- : "denied";
168
+ : r.blocked
169
+ ? "blocked"
170
+ : r.allowed
171
+ ? "allowed"
172
+ : "denied";
198
173
  console.log(` ${r.tool.padEnd(widest)} ${status}`);
199
174
  }
200
175
  console.log("");
201
- console.log(`Summary: ${allowed} allowed, ${denied} denied, ${errored} errored.`);
202
- if (denied > 0) {
176
+ console.log(`Summary: ${allowed} allowed, ${denied} denied, ${blocked} blocked, ${errored} errored.`);
177
+ // Shell sub-tools (issue #76): a compact count rather than 100+ rows.
178
+ let shellDenied = 0;
179
+ if ((0, tool_catalog_js_1.getShellToolCatalog)(harness).length > 0) {
180
+ const shellNamespace = (0, tool_catalog_js_1.resolveShellCommandNamespace)();
181
+ const shellWords = (0, tool_catalog_js_1.getShellCommandCatalog)();
182
+ const shellResults = await probeCatalog(client, shellWords, shellNamespace, subject);
183
+ const sAllowed = shellResults.filter((r) => r.allowed).length;
184
+ shellDenied = shellResults.filter((r) => r.error === undefined && !r.allowed).length;
185
+ const sErrored = shellResults.filter((r) => r.error !== undefined).length;
186
+ console.log(`Shell commands (${shellNamespace}): ${sAllowed} allowed, ${shellDenied} denied, ` +
187
+ `${sErrored} errored of ${shellWords.length}.`);
188
+ }
189
+ if (denied > 0 || shellDenied > 0) {
190
+ console.log("");
191
+ console.log("Denied tools have no grant for this subject. Grant them in the Ory");
192
+ console.log("Console (Agent Security) — the plugin cannot write permissions.");
193
+ }
194
+ if (blocked > 0) {
203
195
  console.log("");
204
- console.log(`Run "${binName} permissions bootstrap" to grant the current user`);
205
- console.log("'use' on every tool in this harness's catalog (idempotent).");
196
+ console.log("'blocked' tools carry an explicit block that overrides any grant.");
197
+ console.log("Remove it in the Ory Console (Agent Security).");
206
198
  }
207
199
  return 0;
208
200
  }
209
201
  async function probeCatalog(client, catalog, namespace, subject) {
202
+ // Evaluate the effective `use` permit, exactly as the runtime gate does.
203
+ const relation = (0, permissions_js_1.resolveCheckRelation)("users");
210
204
  const rows = [];
211
205
  for (const tool of catalog) {
212
- const check = {
213
- namespace,
214
- object: tool,
215
- relation: "use",
216
- ...subject,
217
- };
218
206
  try {
219
- const result = await client.checkPermission(check, {
220
- spanAttributes: { toolName: tool, source: "permissions_status" },
221
- });
222
- rows.push({ tool, allowed: result.allowed });
207
+ const result = await client.checkPermission({ namespace, object: tool, relation, ...subject }, { activityAttributes: { toolName: tool, source: "permissions_status" } });
208
+ let blocked = false;
209
+ if (!result.allowed) {
210
+ // Distinguish an explicit block from a plain missing grant.
211
+ blocked = await client
212
+ .checkPermission({ namespace, object: tool, relation: opl_js_1.RELATION_BLOCKED_SUBJECTS, ...subject }, { activityAttributes: { toolName: tool, source: "permissions_status_block" } })
213
+ .then((r) => r.allowed)
214
+ .catch(() => false);
215
+ }
216
+ rows.push({ tool, allowed: result.allowed, blocked });
223
217
  }
224
218
  catch (err) {
225
219
  const ory = err;
@@ -228,198 +222,39 @@ async function probeCatalog(client, catalog, namespace, subject) {
228
222
  }
229
223
  return rows;
230
224
  }
231
- async function runPermissionsBootstrap(binName, harness, args) {
232
- const dryRun = args.includes("--dry-run") || args.includes("-n");
233
- const resolved = (0, config_js_1.resolveConfig)();
234
- const namespace = resolveNamespace();
235
- const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
236
- if (resolved.auditOnly) {
237
- console.error("Ory is in audit-only mode (kill-switch). Nothing to bootstrap.");
238
- console.error(`Re-enable Ory: ${binName} configure --project-url <URL> --oauth2-client-id <CLIENT_ID> [--api-key <KEY>]`);
239
- return 1;
240
- }
241
- if (!resolved.projectUrl) {
242
- console.error("No ORY_PROJECT_URL configured — cannot write permissions.");
243
- console.error(`Configure first: ${binName} configure --project-url <URL> --oauth2-client-id <CLIENT_ID> [--api-key <KEY>]`);
244
- return 1;
245
- }
246
- if (catalog.length === 0) {
247
- console.error(`No tool catalog known for harness "${harness}".`);
248
- console.error(`Known harnesses: ${tool_catalog_js_1.KNOWN_HARNESSES.join(", ")}`);
249
- return 1;
250
- }
251
- const client = client_js_1.OryAgentClient.fromEnv(harness);
252
- await (0, agent_auth_js_1.ensureAgentIdentity)(client, { projectUrl: resolved.projectUrl }).catch(() => {
253
- /* best-effort; the write may still succeed with the API key or no auth (local Keto) */
254
- });
255
- const subject = (0, subject_js_1.resolveUserSubject)(client);
256
- const subjectId = (0, subject_js_1.subjectLabel)(subject);
257
- if (subjectId === "agent:unknown") {
258
- console.error("No user identity resolved — refusing to write permissions for an unknown subject.");
259
- console.error("");
260
- console.error("Either:");
261
- console.error(" - run the harness once with ORY_USER_LOGIN=true so a user token is cached, or");
262
- console.error(" - set ORY_USER_SUBJECT_ID=<id> to target a known subject.");
263
- return 1;
264
- }
265
- console.log(`Bootstrapping ${catalog.length} permissions for ${harness}`);
266
- console.log("==========================================");
267
- console.log("");
268
- console.log(`Namespace: ${namespace}`);
269
- console.log(`Subject: ${subjectId}`);
270
- console.log(`Tools: ${catalog.join(", ")}`);
271
- if (dryRun) {
272
- console.log("");
273
- console.log("(--dry-run) No permissions will be written.");
274
- }
275
- console.log("");
276
- let created = 0;
277
- let existed = 0;
278
- const failures = [];
279
- for (const tool of catalog) {
280
- const check = {
281
- namespace,
282
- object: tool,
283
- relation: "use",
284
- ...subject,
285
- };
286
- if (dryRun) {
287
- console.log(` ~ would write: ${namespace}:${tool}#use@${subjectId}`);
288
- continue;
289
- }
290
- try {
291
- // Read-before-write: Keto's create API is not idempotent (every PUT
292
- // inserts a new row and never 409s), so a plain createRelationship on
293
- // re-install would duplicate the whole catalog. ensureRelationship
294
- // probes for the exact tuple first and only writes when it is absent.
295
- const spanAttributes = { toolName: tool, source: "permissions_bootstrap" };
296
- let res = await client.ensureRelationship(check, { spanAttributes });
297
- if (res.probeError) {
298
- // Couldn't confirm existence — fall back to a direct write so a
299
- // needed grant is still created (bootstrap's job is to guarantee it).
300
- res = await client.createRelationship(check, { spanAttributes });
301
- }
302
- if (res.alreadyExisted) {
303
- existed++;
304
- console.log(` = ${tool} (already exists)`);
305
- }
306
- else {
307
- created++;
308
- console.log(` + ${tool} (created)`);
309
- }
310
- }
311
- catch (err) {
312
- const ory = err;
313
- const reason = `${ory.code ?? "unknown"}${ory.status ? ` (HTTP ${ory.status})` : ""}`;
314
- failures.push({ tool, reason });
315
- console.log(` ! ${tool} (failed: ${reason})`);
316
- }
317
- }
318
- console.log("");
319
- if (dryRun) {
320
- console.log(`Dry run: ${catalog.length} permissions would be written.`);
321
- return 0;
322
- }
323
- console.log(`Created: ${created} Already existed: ${existed} Failed: ${failures.length}`);
324
- if (failures.length > 0) {
325
- const writeAuthFailure = failures.some((f) => /(unknown|forbidden|session_inactive)/.test(f.reason) ||
326
- /401|403/.test(f.reason));
327
- if (writeAuthFailure) {
328
- console.log("");
329
- console.log("Some writes were rejected — your current credentials may lack");
330
- console.log("write scope on the permission namespace. You can apply the");
331
- console.log("permissions manually using the snippet below (one per line):");
332
- console.log("");
333
- for (const tool of catalog) {
334
- console.log(` ${namespace}:${tool}#use@${subjectId}`);
335
- }
336
- console.log("");
337
- console.log("Or use the Ory Console: open the project, go to Permissions →");
338
- console.log(`Relationships, set namespace='${namespace}', object=<tool>, relation='use',`);
339
- console.log(`and subject='${subjectId}'.`);
340
- }
341
- return failures.length === catalog.length ? 1 : 0;
342
- }
343
- console.log("");
344
- console.log(`Permissions are in place. Promote to enforce mode when ready:`);
345
- console.log(` ${binName} permissions enforce`);
346
- return 0;
347
- }
348
225
  /**
349
- * Whether a user identity is cached locally used by `install` to
350
- * decide whether to opportunistically bootstrap tuples without
351
- * prompting. Returns true when persisted tokens exist and are not
352
- * expired, or when ORY_USER_SUBJECT_ID is set explicitly.
226
+ * Render the trailing source annotation on a Mode line. `server` means a live
227
+ * read from the Ory project; `cache` means the server was unreachable and the
228
+ * last-read value is being used (annotated with when it was fetched); `default`
229
+ * means nothing has ever been read (so `observe`).
353
230
  */
354
- function isUserIdentityCached() {
355
- if (process.env.ORY_USER_SUBJECT_ID)
356
- return true;
357
- const tokens = (0, auth_store_js_1.loadTokens)();
358
- return !!tokens && !(0, auth_store_js_1.isExpired)(tokens);
359
- }
360
- /**
361
- * Print the "permission mode" section of the install banner.
362
- *
363
- * Tells the user what observe vs enforce mean, and prints the two
364
- * follow-up commands they'll typically reach for next:
365
- *
366
- * - `<bin> permissions bootstrap` to seed tuples for the harness's
367
- * built-in tools (the line is shown whether or not we just ran it
368
- * ourselves, so the user can re-run after editing the catalog).
369
- * - `<bin> permissions enforce` once they're satisfied with the
370
- * observe-mode trace output and want denies to actually block.
371
- */
372
- function printPermissionsOnboardingHelp(binName, harness, opts = {}) {
373
- const resolved = (0, config_js_1.resolveConfig)();
374
- const catalog = (0, tool_catalog_js_1.getToolCatalog)(harness);
375
- ui.heading("Permissions:");
376
- ui.summary([
377
- ["Mode", `${resolved.permissionMode}${formatModeSource(resolved.permissionModeSource)}`],
378
- ["Other modes", otherModes(resolved.permissionMode).join(", ")],
379
- ]);
380
- if (resolved.auditOnly) {
381
- ui.hint("Ory is in audit-only mode (kill switch). Permission checks are disabled.");
382
- return;
383
- }
384
- if (resolved.permissionMode === "observe") {
385
- ui.hint("Observe mode: permission denies are logged + traced, but tools still run.");
386
- ui.hint("This is the safe default — promote to 'enforce' once your permission set is correct.");
387
- }
388
- else {
389
- ui.hint("Enforce mode: permission denies block tool execution.");
390
- }
391
- if (opts.bootstrappedAutomatically) {
392
- ui.blank();
393
- ui.success(`Permissions bootstrapped for ${catalog.length} built-in tools.`);
394
- }
395
- else if (catalog.length > 0) {
396
- ui.blank();
397
- ui.bullet("Grant the current user 'use' on every built-in tool (idempotent):");
398
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} permissions bootstrap`);
399
- }
400
- ui.blank();
401
- ui.bullet("Inspect permission coverage:");
402
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} permissions status`);
403
- ui.blank();
404
- ui.bullet("Promote to enforcing once observe-mode logs look right:");
405
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} permissions enforce`);
406
- }
407
- /**
408
- * Render the trailing source annotation on a Mode line. Stays silent
409
- * when the mode came from the built-in default, since saying "default"
410
- * just adds noise. When the mode was overridden by env or config, name
411
- * the override so the reader knows where to look.
412
- */
413
- function formatModeSource(source) {
231
+ function formatModeSource(source, fetchedAt) {
414
232
  switch (source) {
415
- case "env":
416
- return ` (set via ${ENV_PERMISSION_MODE} env var)`;
417
- case "config":
418
- return " (saved in config; change with: permissions observe|enforce)";
233
+ case "server":
234
+ return " (read from the Ory project)";
235
+ case "cache":
236
+ return ` (cached project unreachable${formatFetchedAt(fetchedAt)})`;
419
237
  case "default":
420
- return " (built-in default)";
238
+ return " (default — not yet read from the project)";
421
239
  }
422
240
  }
241
+ /** Human-readable "fetched N ago" suffix for the cached-mode annotation. */
242
+ function formatFetchedAt(fetchedAt) {
243
+ if (!fetchedAt)
244
+ return "";
245
+ const ageMs = Date.now() - fetchedAt;
246
+ if (ageMs < 0)
247
+ return "";
248
+ const mins = Math.floor(ageMs / 60_000);
249
+ if (mins < 1)
250
+ return "; fetched <1m ago";
251
+ if (mins < 60)
252
+ return `; fetched ${mins}m ago`;
253
+ const hours = Math.floor(mins / 60);
254
+ if (hours < 24)
255
+ return `; fetched ${hours}h ago`;
256
+ return `; fetched ${Math.floor(hours / 24)}d ago`;
257
+ }
423
258
  /**
424
259
  * Generic source-suffix helper for other config fields (project URL,
425
260
  * API key). Source 'none' means the value is unset — don't add a suffix.
@@ -430,48 +265,19 @@ function formatSourceSuffix(source) {
430
265
  return " (from env)";
431
266
  case "config":
432
267
  return " (from config)";
268
+ case "default":
269
+ return " (default)";
433
270
  case "none":
434
271
  return "";
435
272
  }
436
273
  }
437
274
  /**
438
- * Lists the modes other than the active one, in stable order, so the
439
- * banner can hint "Other modes: …" without re-listing the active one.
440
- * Audit-only is a separate kill switch (config.auditOnly), but we still
441
- * mention it here so first-time readers learn the full set.
275
+ * Lists the deny postures other than the active one, in stable order, so the
276
+ * banner can hint "Other modes: …" without re-listing the active one. Only the
277
+ * two postures whether Agent Security is connected at all is a separate
278
+ * question, and this is only reached once it is.
442
279
  */
443
280
  function otherModes(active) {
444
- const all = ["observe", "enforce", "audit-only"];
281
+ const all = ["observe", "enforce"];
445
282
  return all.filter((m) => m !== active);
446
283
  }
447
- /**
448
- * If a user identity is already cached and the plugin is configured,
449
- * opportunistically run `permissions bootstrap` so the user lands in a
450
- * "tools work out of the box" state. Otherwise a no-op (the install
451
- * banner still prints the manual command).
452
- *
453
- * Always best-effort: any failure is logged and swallowed so install
454
- * never aborts because of a permissions write problem.
455
- */
456
- async function maybeAutoBootstrap(binName, harness) {
457
- const resolved = (0, config_js_1.resolveConfig)();
458
- if (resolved.auditOnly)
459
- return false;
460
- if (!resolved.projectUrl)
461
- return false;
462
- if (!isUserIdentityCached())
463
- return false;
464
- if ((0, tool_catalog_js_1.getToolCatalog)(harness).length === 0)
465
- return false;
466
- try {
467
- console.log("");
468
- console.log("Bootstrapping permissions (cached user identity detected)...");
469
- const code = await runPermissionsCommand(binName, harness, ["bootstrap"]);
470
- return code === 0;
471
- }
472
- catch (err) {
473
- const msg = err instanceof Error ? err.message : String(err);
474
- console.warn(`Bootstrap skipped: ${msg}`);
475
- return false;
476
- }
477
- }